DEV Community

Reza Lavarian
Reza Lavarian

Posted on • Updated on

Code Splitting with dynamic imports

Defer non-critical resources and boost your page load time.
Over time, as the codebase grows in complexity, the final bundle (the main JavaScript file) would also increase in size.

As you probably know, the web browser, for the most part, is single-threaded, which means all the heavy lifting is done in a single thread, a.k.a the main thread.

That said, the web browser executes JavaScript code in the main thread, the same thread where parsing, layout, and paint happen.

This means if you have a large JavaScript file, the main thread will be busy evaluating your code before the user will be able to interact with the page.

She would have to wait, even though she won't need every single functionality in that bundle right at the beginning.

So, a large JS file = slower page load.

Imagine you have a newsletter subscription form, which pops up once the user clicks on the Subscribe button.

This feature isn't required to load the page, and we don't even know if the user wants to subscribe or not.

That being said, why would want the user to wait for a piece of code she might not use.

Enter Code Splitting

Code Splitting is the process of splitting the code into multiple smaller bundles.

The main benefit of code splitting (among other things) is to have better control over resource load prioritisation - loading the critical ones at load time and loading the others later on.

With code splitting, you'll be able to define what modules should be loaded initially, what modules should be loaded on demand (like when the user clicks on a button), or prefetched when the browser is idle.

If you’re new to modules, a module is a piece of code stored in a file, which you can import into your file to use the functionality it provides — so you won’t have to make everything from scratch.

One approach to code splitting is using dynamic imports.

In Modern JavaScript-based apps, we normally import modules statically.

Let's make it clear with an example.

Imagine we have a piece of code to track the source of the traffic when the user clicks on a button on a landing page.

// ...
import { tracker } from './utils'

let cta = document.querySelector('.cta')

if (cta) {
    cta.addEventListener('click', event => {
        let utmParams = tracker.getUtmParams()
        // Do some cool stuff
    })
}
// ...
Enter fullscreen mode Exit fullscreen mode

The JavaScript snippet above attaches a click event listener to a button with class cta. The handler uses a module named tracker located in the utils file (statically imported) to track the source of the traffic.

A statically imported module such as tracker is included in the main bundle (by your module bundler).

The problem with the above code is that even if the user never clicks on the button, the code is downloaded and executed in the main thread.

That's not very optimal, though.

Let’s rewrite the code with a dynamic approach:

// ...
let btn = document.querySelector('button')

btn.addEventListener('click', e => {
    return import('./tracker' )
    .then(({tracker}) => {
        tracker.getUtmParams()  
    })
})
// ...
Enter fullscreen mode Exit fullscreen mode

This time, the module is dynamically imported as part of the event handler, when the user actually clicks on the button.

When your module bundler (I'm using Webpack for this example) encounters a dynamic import, it bundles the module as a separate file.

It also generates the necessary code (in the main bundle) to load that file dynamically (and asynchronously) later on - through separate HTTP requests.

Note: Import() uses promises internally, so you need to make sure the target browser supports JavaScript promises. To support the older web browsers, you can use a promise polyfill as part of the build process.

However, there's still a small problem.

Since tracker is downloaded in response to an interactive event (mouse click in this case), the user might experience a small lag while the module is being downloaded.

To tackle this issue and make the experience smooth for the user, we can use a resource hint link, to instruct the web browser to prefetch the module at idle time.

Again, if you’re using Webpack (directly or indirectly), you can use an inline directive while declaring your imports, like so:

// ...
let btn = document.querySelector('button')

btn.addEventListener('click', e => {
    return import(/* webpackPrefetch: true */ './tracker' )
    .then(({tracker}) => {
        tracker.getUtmParams()  
    })
})
// ...
Enter fullscreen mode Exit fullscreen mode

This instructs Webpack to inject a resource hint link into your document at run time, to prefetch the module at idle time.

This can be tested in the DevTools:

Screenshot of webpack preloading the JavaScript file

This simple trick, when used correctly, can significantly improve your page's performance metrics, such as Time to Interactive (TTI).

Hope you find this simple trick handy and help you save some time for you and your users.

If you have comments or questions, or if there's something I've gotten wrong, please let me know in the comments below.

Thanks for reading :)

Discussion (0)