Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Asset Versioning & Cache Busting

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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!

23
Login or Register to join the conversation

I loved this course but not some of this is ver messed up and I am not sure what to do. All the errors I receive are very arbitrary and say nothing about line number or what is wrong. PLEASE HELP!!!

Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js):
ModuleBuildError: Module build failed (from ./node_modules/css-loader/dist/cjs.js):

this is in my _global.scss file. No idea what is wrong

****EDIT****

This version only takes css-loader@1.0.1
Works perfectly now

1 Reply
Ian M. Avatar

On IIS we can set an Expires HTTP header for files in a folder - but aren't we now adding a long-lived Expires header to manifest.json too? Is there a way to put the json output in a different directory?

Reply

Hey Ian M.

Honestly I haven't tried it yet, but from Encore docs I can advice to use following example:


     * Encore.configureManifestPlugin((options) => {
     *     options.fileName = '../../var/assets/manifest.json';
     * })

Also you can check following link https://github.com/shellscape/webpack-manifest-plugin to find more configuration options!

Cheers!

Reply
Ian M. Avatar

Thanks Vladimir, but having understood it a bit better I don't think it's a problem anyway - Twig reads manifest.json on the server to set the paths, but nothing on the client reads it. So a long expiry time on the client should be irrelevant

Reply

Wow really nice! However now you know how to change it if needed :)

Cheers have a nice day!

Reply
Patrick Avatar
Patrick Avatar Patrick | posted 4 years ago | edited

Hi Ryan,

I'm working with a load of assets that are compiled by encore, and some others which come from a DIY grunt build process. These are defined as different Asset Packages within my services.yaml, and they have different manifest.json files, and there's a custom Versioning Strategy for the DIY grunt package, as it uses a specific naming convention.

What I'm now trying to do, is push these assets to an S3 bucket when deployment occurs (with Ansistrano, naturally). So I am writing a Symfony Command to push those assets.

What I would really like to do, is iterate over all the asset packages which are defined within my services.yaml, and request the manifest.json path for that package, so that I can consume that json, then hit the package to get the versionized file path, then push it to the correct object path on S3, you get the picture.

I can see that the packages have protected $versionStrategy but no public methods to access it, from where I might be able to request its $manifestPath.

I guess what I really want to do, is for a particular package, request all the file paths which are defined within its manifest, but I can't see any methods to achieve that directly either!

I realise this is a pretty edge case, but wanted to check if there is anything internal I could use before looking into more complex strategies or just using the manifest paths hard-coded (they are defined as parameters, but it seems a shame to have to pass them in again when they are already being passed to the asset packages on construct).

Thanks for the ace tutorial as ever!

Cheers,
Patrick

Reply
Patrick Avatar
Patrick Avatar Patrick | Patrick | posted 4 years ago | edited

So to update this, it turns out that the Asset Packages component does indeed store all the named packages specified in services.yaml but they are a private property.

Therefore, these must be accessed (in the absence of an official API to access this property) either by Reflection (with a call to ->setAccessible(true) on the private ReflectionProperty or - and this is the better way, I think rather! - the Service Definition Object of the Asset Packages service is available during compiler compliation.

Therefore, a custom Compiler Pass can be written and registered in Kernel.php which can access and inspect the configuration of the Asset Packages component, and either set the packages property as a container parameter, or pass it on to another service that we define (like an AssetsPusher service or something like that).

In an ideal world this might be a bundle.. I'll keep you updated on that!

Cheers,
Patrick

Reply

Ohh, wow, nice research Patrick

If you don't want to hack too much you can just inject the manifest path parameter to your command, but yeah, I get your point, it would feel safer if you can just get the path from the Asset Packages

Cheers! and thanks for sharing your findings

Reply

Firstly thank you for the fantastic courses!

I've been coding along but noticed that the cleanupOutputBeforeBuild option installs an older version of CleanWebpackPlugin. However that version doesn't clean the build directory when rebuilding with watch. Installed the new version with:

yarn add clean-webpack-plugin-latest@npm:clean-webpack-plugin --dev

then in webpack.config.js:

const CleanWebpackPlugin = require('clean-webpack-plugin-latest'); and

.addPlugin(new CleanWebpackPlugin({verbose: true, cleanAfterEveryBuildPatterns: ['!static/*', '!fonts/*', '!images/*']}))

This seems to work well except for code splitting (I incorporated the code splitting example from the previous course). When I update anything in the project the old "split" .js file is replaced with a new one and a reference is added to manifest.js (the old reference is not removed) but the manifest.hash.js is not updated.

Is there a way to force webpack to update manifest.hash.js? I tried adding 'manifest.*.js' to the clean patterns but the file is not deleted.

Reply

Hey hurnell!

Thanks for the nice words :). You're totally right about it not cleaning before builds - sort of a known "issue" with that plugin.

About manifest.hash.js, I'm not sure - that manifest.js file should be handled naturally by Webpack - I'm not sure why it's having issues. The other problem is that this manifest.js file is something from Webpack 3, it doesn't exist anymore on Webpack 4 (it's just that code splitting is done a slightly different way). So, you might have problems finding a way around this, because it's on an older version :/.

Let me know what you find out!

Cheers!

Reply
Dirk Avatar

Love it! Being able to deal with assets in a modular way is really great. There is one thing I wonder about
(this might be a stretch..). To optimize ever further, would there be a way of telling which parts of your external assets your app actually uses and only include those? I mean, jquery, bootstrap and font-awesome are all quite ' big' and I probably only use a few percent of what they have to offer. Could i include only those parts somehow?

Reply
Dirk Avatar

One last thing I noticed: when you use .enableVersioning() (which works GREAT!) in PHPStorm I do see in the templates that the asset is missing (because the actual filename is different). Any option on getting rid of this warning?

Reply

Hey Dirk

About your first question. I don't really know the answer, I've heard something about "tree shaking" but I'm don't know too much on the topic. It may help you out: https://webpack.js.org/guides/tree-shaking/

About your second question, if you upgrade Encore, then you will have a new function for including assets into your templates.


// some template.html.twig
{% block javascripts %}
    {{ parent() }}

    {{ encore_entry_script_tags('your_entry_point_name') }}
{% endblock %}

I hope it helps. Cheers!

Reply
Dirk Avatar

I'm gonna read more about 'tree shaking' . Your answer to the second question works well! The only file missing in entrypoints.json is the shared entry (or Layout in this tutorial), but I guess this is because nowadays we should use splitEntryChunks() instead.

Reply

Yep, the "shared entry" technique is a bit dated now. If you want to dive deeper, you definetely should watch this SymfonyCon talk: https://symfonycasts.com/sc...

Have a great weekend Dirk. If you find something useful about "Tree Shaking" please let me know!

Reply
Cluster Avatar
Cluster Avatar Cluster | posted 4 years ago

Thank you for this Encore course!

I have a question regarding the use of babel and autoprefixer via Encore. The symfony docs on Encore recommend using the browserslist key in package.json for postcss's autoprefixer, however the babel docs recommend using the .browserslistrc file. I'd like to set up Encore to use the same browserslist for the style and script assets, preferably with only one configuration source. What's the recommended procedure?

Also, I'm not sure I understand the docs (https://symfony.com/doc/cur... correctly about the babel configuration. If I do nothing, then babel is enabled (?), but I do still need to install the babel preset, but not use create the .babelrc, because it will override the default Encore babel config? I'm confused. Normally with vanilla Webpack I would run `npm run --save-dev @babel/preset-env` then create the .babelrc to enable the env preset and use the babel-loader in the Webpack js rule. With the current Encore, I can't use @babel/preset-env but need to use the older babel-preset-env (right?) and do I yes or no need the .babelrc file and what do I need to do vis-à-vis Encore to make it all work? I'm just not sure how much "magic" Encore does behind the scenes (does it parse the package.json to see what babel presets were installed or do you use install the preset and then use .configureBabel() but then duplicate the browserslist config in webpack.config.js)?

Maybe I'm just confused with the different moving parts, but I'd really appreciate your tips and insights. :)

Reply

Hey Cluster!

Ah, sorry for my slow reply! And SUCH good questions! I was just working on this part of the docs last night :).

So, yes, there are a lot of moving pieces here... especially because Encore will soon have a new version... where things will drastically simplify. A few notes:

1) Yes, babel-preset-env is currently auto-configured for you in Encore. In the next version of Encore, we will upgrade to @babel/preset-env. You do not (now or then) need to create a .babelrc file - we do that configuration behind the scenes (and you can further the configuration via a configureBabel() method, if you need to).

2) Even though Babel says you should create a .browserslistrc file, Babel also reads the browserslist key in package.json. I'm honestly a little surprised that they recommend the .browserlistsrc file, as other tech seem to prefer the browserslist key. Anyways, it doesn't really matter: the browserslist library is able to find your config in package.json or .browserslistrc. Choose whatever you want, but I recommend the package.json key.

3) However! This won't work :). The current version of Encore is built in an older version of Babel that did NOT properly read the browserslist config. You're reading the documentation for the new version. But, don't worry - as soon as we release the next version of Encore, you'll be using the new Babel and the browserslists stuff will work.

So, to summarize:
1) Just use the browserslist package.json file (though you can use the config file if you want).
2) Yes, the babel env preset is automatically applied by Encore
3) You will need to wait for the next Encore release (coming soon - been working a bunch on the last details!) before Babel will properly read the browserslist config.

Phew! Does that answer everything? Let me know!

Cheers!

Reply
Cluster Avatar

Hi Ryan!
Many hearty thanks for the thorough and complete response. Yep, everything's cleared up and crystal. Great job on the new Encore version!! I'm looking formward for the update! Keep up the super work!!! :D Cheers!

Reply
Steve-D Avatar
Steve-D Avatar Steve-D | posted 5 years ago

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

Reply

Hey Steve D.!

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!

Reply
Steve-D Avatar

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

Reply

Hey Steve D.!

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!

Reply
Steve-D Avatar

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.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.8.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
        "friendsofsymfony/user-bundle": "dev-master", // dev-master
        "sensio/framework-extra-bundle": "^5.1", // v5.1.5
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.0.4
        "symfony/framework-bundle": "^4.0", // v4.0.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/monolog-bundle": "^3.1", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.7.0
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/swiftmailer-bundle": "^3.1", // v3.1.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/validator": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0", // v4.0.4
        "twig/twig": "2.10.*" // v2.10.0
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.4
        "symfony/dotenv": "^4.0", // v4.0.4
        "symfony/phpunit-bridge": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0" // v4.0.4
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.19.0", // 0.19.0
        "bootstrap": "3", // 3.3.7
        "copy-webpack-plugin": "^4.4.1", // 4.4.1
        "font-awesome": "4", // 4.7.0
        "jquery": "^3.3.1", // 3.3.1
        "node-sass": "^4.7.2", // 4.7.2
        "sass-loader": "^6.0.6", // 6.0.6
        "sweetalert2": "^7.11.0", // 7.11.0
        "webpack-notifier": "^1.5.1" // 1.5.1
    }
}
userVoice