Cache busting using Laravel Mix and CloudFront+S3

Dec 14, 2018 LaravelAWS

When you build assets (js, css, images) in Laravel applications, Laravel Mix is very useful. Laravel Mix is a wrapper for Webpack and you can build Vue.js components, Sass and images out of the box.

For basic usage, you don’t need to modify the default configuration file, webpack.mix.js. However, when you deploy an app to production, it’s important to take into account cache busting.

In this post, I’ll show you how to implement cache busting using Laravel Mix and Amazon CloudFront+S3.

Cache busting using Laravel Mix

The default config for Laravel Mix is something like this. You can build JS (including Vue.js components) and CSS out of the box!

const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
  .sass('resources/sass/app.scss', 'public/css');

However, it’s not enough for production app. Browsers cache assets for a long period of time unless URL changes, even if you deploy new assets. So it’s very important to force browsers to load the fresh assets instead of serving stale copies of the code. It’s called cache busting. If you are not familiar with cache busting, read this article.

With Laravel Mix, you can implement cache busting by calling mix.version() like below.

const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
  .sass('resources/sass/app.scss', 'public/css');

if (mix.inProduction()) {
  mix.version();
}

It appends version hash as a query parameter like ?id=28bb485e844345258. Version hash won’t change unless file contents change. That means that assets URL will change when file contents have any updates. As a result, browsers will load fresh contents instead of the cached stale assets.

Serve assets via CDN

The example in the previous section assumes assets are served directly from web servers. That’s o.k for applications that don’t have a large number of traffic. However, if your application has high traffic or performance is critical, you should serve assets from CDN.

In my case, I use Amazon CloudFront and S3 for CDN. By default, Laravel Mix doesn’t cover uploading assets to S3, so I need additional setup.

First, install webpack-s3-plugin.

$ yarn add webpack-s3-plugin

Then, create S3 configuration and pass it to mix.webpackConfig().

const mix = require('laravel-mix');
const s3Plugin = require('webpack-s3-plugin');

let webpackPlugins = [];
if (mix.inProduction() && process.env.UPLOAD_S3) {
  webpackPlugins = [
    new s3Plugin({
      include: /.*\.(css|js)$/,
      s3Options: {
        accessKeyId: process.env.AWS_KEY,
        secretAccessKey: process.env.AWS_SECRET,
        region: 'ap-northeast-1',
      },
      s3UploadOptions: {
        Bucket: process.env.ASSETS_S3_BUCKET,
        CacheControl: 'public, max-age=31536000'
      },
      basePath: 'app',
      directory: 'public'
    })
  ]
}

mix.js('resources/js/app.js', 'public/js')
  .sass('resources/sass/app.scss', 'public/css');

mix.webpackConfig({
  plugins: webpackPlugins
});

if (mix.inProduction()) {
  mix.version();
}

I get AWS credentials and bucket name from .env file.

AWS_KEY=YourKey
AWS_SECRET=YourSecret
ASSETS_S3_BUCKET=YourBucketName

In package.json, I have two scripts for production build. The first one builds assets for production but doesn’t upload to S3. The other one uploads to S3 because it sets UPLOAD_S3=true.

"scripts": {
    "production-build": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "production-upload-s3": "cross-env NODE_ENV=production UPLOAD_S3=true node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
}

Now you can build assets and upload them to S3, but how do you reference CDN assets in blade template? Even though Laravel comes with mix() helper function, it references assets in local server, not the outside location.

Therefore, I implemented a custom helper function that wraps mix() and returns CDN asset URL. I defined CDN base URL in config/assets.php file.

<?php

if (! function_exists('asset_path')) {
    /**
     * Get the path to a versioned Mix file.
     *
     * @param  string  $path
     * @param  string  $manifestDirectory
     * @return \Illuminate\Support\HtmlString|string
     *
     * @throws \Exception
     */
    function asset_path($path, $manifestDirectory = '')
    {
        $mixPath = mix($path, $manifestDirectory);
        $cdnUrl  = config('assets.cdn_url');
        $env     = config('app.env');
        // Reference CDN assets only in production or staging environemnt.
        // In other environments, we should reference locally built assets.
        if ($cdnUrl && ($env === 'production' || $env === 'staging')) {
            $mixPath = $cdnUrl . $mixPath;
        }
        return $mixPath;
    }
}

It references CDN assets only in production and staging environment since I wanted to reference local assets during development. You can use it in blade templates like this 🙂

<script src="{{ asset_path('js/app.js') }}"></script>

Setup CloudFront+S3

Now you can upload assets to S3 and reference asset URL in CDN. If you haven’t setup CloudFront and S3, the basic setup is described in the AWS official docs.

Cache based on query string parameters

By default, CloudFront doesn’t cache contents based on query string parameters. Let’s say, if you have app.css?id=1111 and app.css?id=2222, CloudFront ignores id parameter and just creates cache as app.css.

That means that when you upload a new version of app.css to S3, CloudFront won’t update the cache and will serve stale contents to the client unless you invalidate the cache.

However, you can configure CloudFront to cache contents based on query string parameters. CloudFront will create cache for app.css?id=1111 and app.css?id=2222. A new cache will be created every time you deploy new version of your app.

Caching Content Based on Query String Parameters

So, let’s edit CloudFront settings!

Click distribution ID you want to edit

Click distribution ID you want to edit

In the “Query String Forwarding and Caching” section, select “Forward all, cache based on whitelist”, add id to "Query String Whitelist" then update the behavior. Laravel Mix adds id query string to each asset url as a version hash.

img

Query String Forwarding and Caching

What about deployment?

It depends on how your deployment workflow is. In my case, I build assets and upload to S3 by executing yarn run production-upload-s3 on CI.

avatar

About Me

I am a software engineer from Japan and currently based in Singapore. Over the past 5 years, I've been creating e-commerce and FinTech web applications using Laravel, Angular, and Vue.js. Making clean, maintainable, and scalable software with agile methodology is one of my biggest passions. Im my spare time, I play the bass and enjoy DIY.