Long term caching with webpack and templates

Saturday, Apr 22 2017 in frontend

Far future expiry on your CSS and JS resources ensure visitors load them from cache as often as possible, improving page speeds and reducing bandwidth costs. But it can be complicated to force visitors to load a new version before the old one expires. CDN amplify the issue. When you distribute your assets to a CDN like Cloudfront, the CDN servers all over the world only request a new copy when the assets expire. If you want to spread a new version of a file to the CDN before the old expires, you need to issue an invalidation request; invalidation is not instantaneous and might even cost money.

You can solve this by appending an hash of the file contents to the file name, for example:

 app-bundle-902805999c154c77880d.js

In this way, every new version will have a different file name, so you do not need to replace old ones.

The webpack asset bundler can generate a hash of the source file contents and append it to the output file, but now you’ve got to modify your <script> tags to point to a new filename. HTMLWebpackPlugin solves this problem by also generating the <script> tag. This also works to include your assets in a file that uses a template language like Freemarker or Go templates instead of plain HTML.

Install HTMLWebpackPlugin:

npm i html-webpack-plugin

In the webpack configuration file, change the output name to contain the hash of the file contents:

module.exports = {
  ...
  output: {

    filename: '[name]-bundle-[chunkhash].js'
  }
  ...
}

Then import HTMLWebpackPlugin at the top of the webpack configuration:

const HTMLWebpackPlugin = require('html-webpack-plugin');

and add a new entry to the plugins array:

module.exports = {
  output: {...},
  module: {...},
  plugins: [new HtmlWebpackPlugin({
    template: 'static/scripts-template.html', 
    inject: false, 
    filename: 'scripts.html'})]
}

template sets the path to the template that HTMLWebpackPlugin uses to generate the file that includes the <script> tag. inject: false tells the plugin to not automatically insert the include tags in the output. filename sets the path where HTMLWebpackPlugin generates its own output.

In, static/scripts-template.html, use HTMLWebpackPlugin own integrated template language to output the <script> tags you want:

<% htmlWebpackPlugin.files.js.forEach(function(file) { %>
<script src="<%= file %>"></script>
<% }); %>

This outputs a <script> tag for every generated JavaScript file. Alternatively, if you just want to output every file that webpack generates, including CSS, yu can leave the template entirely empty and set inject: true and HTMLWebpackPlugin automatically insert the appropriate <style> and <script> tags.

After you run the webpack build, the generated scripts.html file will just contain the script tags. You can then import it in any server-side templating language, for example, with Go:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  <link href="styles.css" rel="stylesheet"></head>
  <body>
  <div id="app">
  </div>
  {{ template "scripts.html" }}
</body>
</html>

In this way, you reap the caching benefits of hashed file names without replacing by hand the paths in your HTML files.