Asset Versioning & Cache Busting

There is one last thing I want to talk about, and it's one of my favorite features in Encore. Here's the question: how can we version our assets? Or, even more simple, how can we bust browser cache? For example, right now, if I change something in RepLogApp.js, then of course Webpack will create an updated rep_log.js file. But, when an existing user comes back to our site, their browser might use the old, cached version! Lame!

Enabling Versioning

This is a classic problem. But with Encore, we can solve it beautifully and automatically! In webpack.config.js, first add .cleanupOutputBeforeBuild():

... lines 1 - 3
Encore
... lines 5 - 25
.cleanupOutputBeforeBuild()
... line 27
;
... lines 29 - 32

That's a nice little function that will empty the public/build directory whenever you run Encore. Then, here's the key: .enableVersioning():

... lines 1 - 3
Encore
... lines 5 - 25
.cleanupOutputBeforeBuild()
.enableVersioning()
;
... lines 29 - 32

That's it! Because we just changed our config, restart Encore:

yarn watch

Now look at the build/ directory. Woh! Suddenly, all of our files have a hash in the filename! The hash is based on the file's contents: so whenever the file changes, it gets a new filename. This is awesome! Now when rep_log.js changes, it will have a new filename. And when we deploy to production, the user's browser will see the new filename and load it, instead of using the old, cached version.

Versioned Filenamed with manifest.json

Perfect! Except... we just broke everything. Find your browser and refresh. Yep! It's horrible! And this makes sense: in the base layout, our script tag simply points to build/layout.js:

... lines 1 - 96
{% block javascripts %}
... lines 98 - 101
<script src="{{ asset('build/layout.js') }}"></script>
{% endblock %}
... lines 104 - 107

But this is not the filename anymore - it's missing the hash part!

Of course, we could type the filename manually here. But, gross! Then, every time we updated a file, we would need to update its script tag.

Here's the key to fix this. Behind the scenes, as soon as we started using Encore, it generated a manifest.json file automatically. This is a map from the source filename to the current hashed filename! That's great! If we could somehow tell Symfony's asset() function to read this and make the transformation, then, well... everything would work perfectly!

And... yea! That feature exists! Open config/packages/framework.yaml. Anywhere, but I'll do it at the bottom, add assets: then json_manifest_path set to %kernel.project_dir%/public/build/manifest.json:

framework:
... lines 2 - 35
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

This is a built-in feature that tells Symfony to look for a JSON file at this path, and to use it to lookup the real filename. In other words... just, refresh! Yea, everything is beautiful again! Check out the page source: it's using the hashed filename from the manifest file.

And if you change one of the files - like layout.js: add a console.log()... as soon as we do this, Webpack rebuilds. In the build/ directory - you might need to synchronize it, but yes! It creates a new filename. When you refresh, the system automatically uses that inside the source.

Long-Lived Expires Headers

This is free asset versioning and cache busting! If you want to get really crazy, you can also now give your site a performance boost! To do that, you'll need to configure your web server to set long-lived Expires header on any files in the /build directory.

Basically, by setting an Expires header, your web server can instruct the browser of each user to cache any downloaded assets... forever! Then, when the user continues browsing your site, it will load faster because their browser knows it's safe to use these files from cache. And of course, when we do make a change to a file in the future, the browser will download it thanks to its new filename.

The exact config is different in Nginx versus Apache, but it's a common thing to add. Google for "Nginx expires header for directory".

OK guys, I hope, hope, hope you love Webpack Encore as much as I do! It has even more features that we didn't talk about, like enableReactPreset() to build React apps or enableVueLoader() for Vue.js. And we're adding new features all the time so that it's easier to use front-end frameworks and enjoy some of the really amazing things that are coming from the JavaScript world... without needing to read 100 blog posts every day.

So get out there and write amazing JavaScript! And I hope you'll stay with us for our next tutorial about React.js & Symfony!

All right guys, seeya next time!

Leave a comment!

  • 2018-07-05 Steve

    Hi Ryan, sorry for not coming back to you sooner, works gone from mental to super mental ha ha. I gave up in the end as it was very much a nice to do rather than needed but thank you for supporting. I'm sure I'l be back soon with something else for you.

  • 2018-06-05 weaverryan

    Hey Steve!

    If you're comfortable sending any of your code over here to the comments (even though it's not Symfony), we'd be happy to take a look :).

    Cheers!

  • 2018-06-05 Steve

    Hi Ryan, thank you for the reply. Amazing how easy it is in Symfony but after spending a few hours on trying to sort (not one to give up) I've given up. I suspect it is rather easy to do and I'm just missing a vital ingredient.

    Thanks again

  • 2018-06-04 weaverryan

    Hey Steve!

    Woohoo! Really glad to hear about your success! And, using Encore outside a Symfony project - *awesome*.

    So, Encore is totally independent of Symfony... except for the versioning part as you have figured out ;). Here's what happens:

    1) When you enable versioning in Encore, suddenly all of your filenames have hashes in them - like main.abc123.js
    2) The problem is... how can you create a script tag for a filename that is constantly changing? If you hardcode the main.abc123.js, then when that filename changes, your script tag is broken!
    3) To help solve this problem, Encore writes a manifest.json file in your build directory that is a map from the source filename (e.g. main.js) to the current hash filename (e.g. main.abc123.js). But, you need to make your backend code read this somehow.
    4) In Symfony, we built a small system that does this: if you enable that configuration in framework.yaml, when you say

    {{ asset('build/main.js') }}

    , it actually looks in the manifest.json file for this "build/main.js" key, and replaces it with hashed filename. This means that, in your final HTML, you always have the current, versioned filename.

    So, if you're outside of Symfony, you need to manually write some sort of function that you use when rendering your script/link tags that will go look at the manifest.json file and do the same replacement that Symfony is doing. You'll need to write this, but fortunately, it's pretty easy logic. But, let me know if you have questions!

    Cheers!

  • 2018-06-04 weaverryan

    Matt Johnson Woohoo! Thanks for the nice note Matt!

  • 2018-06-03 Matt Johnson

    I am super impressed with Encore. Webpack seemed so complex/difficult to manage in the last tutorial. Encore makes it a breeze.

    Thanks for another great tutorial!

  • 2018-06-02 Steve

    Hi Ryan

    I've been following along and loving the series. You've got me using PHPStorm too which is awesome. Heading on to the doctrine tuts next and really looking forward to them.

    I am also using WebPack Encore in a none Symfony project and up until this tutorial was doing fine. Now I've added the versioning it breaks as there is no config/packages/framework.yaml and even adding it it doesn't work.

    Are you able to assist in how I would get this working? Do I need to reference the framework.yaml somewhere?

    Thanks

  • 2018-04-04 weaverryan

    Hey Allen!

    > I guess what Im asking is if encore/webpack has some features for this, or will i just have to import each library into each file for now?

    Ah yes, I understand now - and I get this question with some frequency. Short answer: yes, you need to require/import a module in each file that you need to use it. It's not a Webpack/Encore thing, it's more of a "this is how you're supposed to code in Node" thing. I would make a parallel to using namespaces in PHP: it's more-or-less just "how you code" in PHP, and everything works by using them :).

    Longer answer, yes, the autoProvideVariables() *does* actually make this possible. But, here's how it works under the hood. If you added swal to autoProvideVariables(), then whenever Webpack sees an uninitialized swal variable anywhere in your code, it (basically( adds const swal = require('sweetalert2') right above that line. In other words, you DO need require/import statements.... but this trick is a way for Webpack to "fix" your code for you ;).

    Cheers!

  • 2018-03-31 Allen

    Yes exactly, something like sweet alert that exports the variable for each instance (&& LoDash) im guessing would need to autoProvideVariable but the problem is something like sweetalert is only used in every couple of files, and Lodash has the possibility of being used in many files.

    As well i have things like Bloodhound and typeahead that i want to use in many parts of my application. Instead of adding it to each file. One exports a variable (Bloodhound) and the other does not and should be global (typeahead) but neither work outside the file. I guess what Im asking is if encore/webpack has some features for this, or will i just have to import each library into each file for now?

    Thanks so much for the blog info! Ill be checking stuff out almost weekly. This is awesome and such a great tool, love your videos, so awesome!

  • 2018-03-21 weaverryan

    Hey Allen!

    Haha, you just made my morning! :D :D

    > You should do one on the autoProvideVariables function since it pretty much helps auto add jquery and bootstrap (popper,js), for all your "applications" or files. Had to do some research for that feature.

    I didn't cover this, but we did cover `autoProvidejQuery()`, which is just an alias that auto-provides jquery... which is what you need 99% of the time. But, I didn't think about popper.js for Bootstrap. We may even need/want to add an `autoProvidePopper` method, because that's probably going to be a really common use-case.

    > Also is there a way to do this with sweetalert2? Or something like this that someone might need on all pages like the bootstrap button feature.

    What do you mean exactly? Do you just want to be able to use the `swal` variable without requiring it in each file? Or, something different?

    > Also where might we find a list of upcoming, new, non-built-in, and cool features that are available??? Will it be updating on the Symfony site?

    Ah, cool question! For the big features, you'll probably see something on the Symfony.com blog. The features are hidden in the issue list - which is a bit large right now - https://github.com/symfony/.... The big ones include Webpack 4 support (Webpack 4 is supposed to have MUCH faster builds), hopefully HMR support for CSS (this is when your CSS styles reload on the page without refreshing), possibly a "generator" so that you can generate starting React/Vue apps... and probably a few other things. Fun stuff - just need to fine some time :).

    Cheers!

  • 2018-03-21 Allen

    This is AMAZING!!!!! Been waiting for this tutorial for about 4 months. Thanks so much Ryan, this is sickkkkk. Especially without knowing about the buildNotification feature.
    You should do one on the autoProvideVariables function since it pretty much helps auto add jquery and bootstrap (popper,js), for all your "applications" or files. Had to do some research for that feature.
    Also is there a way to do this with sweetalert2? Or something like this that someone might need on all pages like the bootstrap button feature.

    Also where might we find a list of upcoming, new, non-built-in, and cool features that are available??? Will it be updating on the Symfony site?