2021/06/28

Lazy Loading Vanilla JS with WebPack (+ Laravel Mix)

optimisationJavaScript

I recently worked on a project where the site was incredibly JavaScript heavy, this caused the page to be requesting ~3mb of JavaScript every page load.

Since the site was a commerce site, this would have been killer and caused a lot of lost sales, so I had to find a way to optimise the script.

Installation

I use Webpack for pretty much all of my projects these days. Webpack is powerful but frankly a pain in the ass to configure, so to navigate this I use Laravel Mix which coats the config into a nice, user-friendly, script.

If you don't have these installed so far, you can ahead and get things kicked off by installing laravel-mix.

npm install laravel-mix --save-dev

Followed by creating a webpack.mix.js file to house our configuration.

touch webpack.mix.js

We can set up some barebones configuration by telling Mix what file we want to listen to and where we want it output.

const mix = require('laravel-mix')

mix.js('resources/js/site.js', 'public/assets/js')

If you've already got this set up on your end, you can ignore the above steps.

Lazy Loading

Sweet, we've got everything set up that we need in our webpack file. Next, we need to configure how we are going to lazy load things.

In the above config, we're looking for a site.js file, this will house our logic for importing JavaScript.

Let's first look at an example script we might want to import.

Imported Script

I like writing my components/partials as classes that house all of their logic, this stops there being any conflicts between classes relating to different components.

I then extract any common code into a helpers.js file, but this is an article for another day.

Back to our lazy loading. Let us say we had a script to handle a testimonial component. Our basic script may look something like the following:

class Testimonials {
  constructor() {
    console.log('i load')
  }
}

export default Testimonials

Perfect, we've got a class and we are exporting it for use.

Site.js

In our main file, if the JavaScript was global and used on every page we may do something like a simple import and initialise.

import Testimonials from  './partials/Testimonials'

new Testimonials()

This is fine if the JS is used on every page, but our partials won't. To only load this whenever the script is needed, we need to ensure the code is on the page.

We could do this one of many ways, but fundamentally we're just making sure an element exists in the DOM.

import Testimonials from  './partials/Testimonials'

if (document.querySelector('.testimonials')) {
	new Testimonials()
}

Okay, but we're not there yet. We're still bringing the JavaScript into our main bundle. Even if we aren't calling the code, it's still there taking up space.

To ensure we only load the JS when needed, we need to rewrite our import function.

if (document.querySelector('.testimonials')) {
  import(
    './partials/Testimonials'
  ).then((Testimonials) => {
    new Testimonials.default()
  })
}

 

You may notice the .default appended to the module here, this is because when using import() on ES6 modules you must reference the .default property as it's the actual module object that will be returned when the promise is resolved.

Great, now when compiled this will only include a small bit of code telling the browser that if it finds the element with a class of .testimonials then we need to load in another script.

Compiling

Great, if we run mix we should see the code compiling. Once done you should get something like the following.

Screenshot of terminal window with output unoptimised.

This works, but this isn't exactly a clean output. I like to keep my partials neatly contained in their own folder, and with a better name than resources_js_partials_Testimonials_js.js.

Fortunately, we can fix this by including a small comment in the import function.

if (document.querySelector('.testimonials')) {
  import(
    './partials/Testimonials' /* webpackChunkName: "partials/testimonials" */
  ).then((Testimonials) => {
    new Testimonials.default()
  })
}

If we recompile, we should see the partial is compiled to js/partials/testimonials.js

Screenshot of terminal window with output optimised.

How to Use

We don't need to import each of these partials into the page, as long as we are including our main output file, in this case site.js. It'll automagically know where and when to import the additional scripts.

If you take a look at the network tab in your browser, you should only see that chunk (partials/testimonials.js) loaded when the querySelector returns something.

Closing Words

I hope this helps you with optimising JavaScript bundles on your site, I know this has helped our websites load a lot better on mobile devices/slow connections especially.

There are so many ways we can optimise our sites on the modern web, however, we sometimes skip past these steps, and the user suffers. For a relatively small code change, you can achieve some big results.

Feel free to share this or reach out to me on Twitter to discuss this.