Learn Webpack by Example: Simple Code-Splitting in a Vanilla JavaScript App

Webpack Logo

As a professional developer, I‘ve seen firsthand how critical application performance is to the success of any web-based project. Users today expect fast, responsive experiences across all devices, and even a slight delay can lead to frustration and abandonment.

One of the most effective techniques for optimizing the loading performance of JavaScript applications is code splitting. By splitting your application code into multiple bundles that can be loaded on demand or in parallel, you can significantly reduce initial load times and improve overall responsiveness.

Understanding Webpack and Code Splitting

Webpack is a powerful static module bundler for JavaScript applications. At its core, Webpack takes all your application‘s dependencies (JavaScript files, CSS, images, etc.) and generates optimized bundles that can be efficiently loaded by a web browser.

One of Webpack‘s most valuable features is its built-in support for code splitting. Code splitting allows you to divide your application code into multiple chunks or bundles. These bundles can then be loaded on demand as the user interacts with your application, rather than loading everything upfront.

The main benefits of code splitting are:

  1. Faster initial load times: By splitting your code, you reduce the amount of JavaScript that needs to be downloaded and parsed by the browser when a user first visits your site. This can lead to dramatically faster page loads, especially on slower networks or devices.

  2. More efficient caching: When your code is split into multiple files, browsers can cache those files separately. This means that if you update one part of your application, users only need to re-download that specific chunk rather than the entire app.

  3. Better resource utilization: Code splitting allows you to prioritize the loading of critical application code and defer the loading of less important modules. This ensures that your application is interactive as soon as possible, even if some parts are still loading in the background.

A Simple Code Splitting Example

To demonstrate the basics of code splitting with Webpack, let‘s walk through a simple example. We‘ll start with a basic vanilla JavaScript application that has two routes: a home page and an about page.

Here‘s what our initial project structure might look like:

my-app/
  dist/
    index.html
  src/
    index.js
    home.js
    about.js
  webpack.config.js
  package.json

In this setup, index.js serves as the entry point for our application, while home.js and about.js contain the code for our two routes.

Here‘s what the content of these files might look like:

// index.js
import Home from ‘./home‘;
import About from ‘./about‘;

const routes = {
  ‘/‘: Home,
  ‘/about‘: About
};

function router() {
  const path = window.location.pathname;
  const Component = routes[path];

  if (Component) {
    const root = document.getElementById(‘root‘);
    root.innerHTML = Component();
  }
}

window.addEventListener(‘popstate‘, router);
router();


// home.js
export default function Home() {
  return `

    <p>Welcome to the home page!</p>
    <a href="/about">About</a>
  `;
}


// about.js
export default function About() {
  return `

    <p>This is the about page.</p>
    <a href="/">Home</a>
  `;
}

To bundle this application with Webpack, we‘d use a configuration like this:

// webpack.config.js
const path = require(‘path‘);

module.exports = {
  mode: ‘development‘,
  entry: ‘./src/index.js‘,
  output: {
    path: path.resolve(__dirname, ‘dist‘),
    filename: ‘bundle.js‘
  }
};

Running webpack with this configuration would generate a single bundle.js file containing all our application code. However, this means that even if a user only visits the home page, they still have to download the code for the about page as well.

To fix this, we can use Webpack‘s dynamic import() syntax to split our about page into a separate chunk:

// index.js
import Home from ‘./home‘;

const routes = {
  ‘/‘: Home,
  ‘/about‘: () => import(‘./about‘)
};

// ...

Now, Webpack will generate two bundles: bundle.js for the main application code and a separate chunk for the about page. The about page code will only be downloaded if the user navigates to the /about route.

The Impact of Code Splitting

To see the impact of code splitting, let‘s consider some real-world performance statistics.

In a study conducted by Google, they found that for every second delay in mobile page load, conversions can fall by up to 20%. That means if your site takes 5 seconds to load, you could be losing up to 60% of your potential conversions.

Code splitting can have a significant impact on load times. Let‘s say our original application bundle was 1MB. By splitting out a 200KB about page, we‘ve reduced the initial download size by 20%. For users on slow 3G connections, this could save several seconds of load time.

But the benefits don‘t stop there. Code splitting also improves caching efficiency. Let‘s say we make a change to our about page. With our original bundle, users would have to re-download the entire 1MB file. But with code splitting, they only need to download the updated 200KB chunk.

Over time, these savings add up. Twitter, for example, saw a 40% reduction in their initial JavaScript payload size after implementing code splitting. This led to a 10-20% improvement in Time to Interactive (TTI) for their mobile site.

Advanced Code Splitting Techniques

While the basic import() syntax is powerful, Webpack offers even more advanced code splitting features through its splitChunks optimization.

With splitChunks, you can control exactly how Webpack splits your code. For example, you might want to create separate bundles for your application code, third-party libraries, and shared modules.

Here‘s what a splitChunks configuration might look like:

// webpack.config.js
module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: ‘all‘,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: ‘vendors‘,
          chunks: ‘all‘
        },
        common: {
          name: ‘common‘,
          minChunks: 2,
          chunks: ‘async‘,
          priority: 10,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    }
  }
};

This configuration does a few things:

  1. It creates a separate vendors chunk for any modules in the node_modules directory (i.e., your third-party dependencies).

  2. It creates a common chunk for any modules that are used in at least two places (minChunks: 2) and are loaded asynchronously (chunks: ‘async‘).

  3. It prioritizes the common chunk over others (priority: 10) and enforces its creation even if it‘s empty (enforce: true).

By fine-tuning your splitChunks configuration, you can ensure your bundles are split optimally for your specific application.

Best Practices and Pitfalls

While code splitting is a powerful technique, it‘s not without its pitfalls. Here are a few best practices to keep in mind:

  1. Profile your bundles: Use Webpack‘s BundleAnalyzerPlugin to visualize the contents of your bundles. This can help you identify large or unnecessary modules that are candidates for splitting.

  2. Don‘t over-split: While it‘s tempting to split your code as much as possible, each additional chunk comes with some overhead. Aim for a balance between initial bundle size and the number of on-demand chunks.

  3. Cache your bundles: Ensure your web server is setting appropriate cache headers for your JavaScript files. This allows browsers to store your bundles efficiently and avoid unnecessary re-downloads.

  4. Lazy load judiciously: Lazy loading can significantly improve initial load times, but it can also lead to jarring user experiences if not handled carefully. Consider using placeholders or skeleton screens while lazy loaded modules are downloading.

Here‘s what Addy Osmani, Engineering Manager at Google and author of the "JavaScript Start-up Performance" guide, has to say about code splitting:

"Code-splitting is one of the most compelling features of bundlers like webpack. It‘s a fantastic way to trim down your initial bundle size and ship just the code a user needs for a route or a feature. It‘s not just for single-page apps either – websites that progressively enhance can lazy-load additional code."

Real-World Examples

Many high-profile websites and applications have seen significant performance improvements by implementing code splitting. Here are a few examples:

  • Twitter: As mentioned earlier, Twitter reduced their initial JavaScript payload by 40% using code splitting, leading to a 10-20% improvement in Time to Interactive.

  • Tinder: Tinder used code splitting to reduce their initial bundle size by 60%. This led to a 50% reduction in load times for their web app.

  • Uber: Uber implemented code splitting in their web dashboard, reducing the initial JavaScript bundle size by 20%. They also saw a 25% improvement in Time to Interactive.

  • Airbnb: Airbnb used code splitting and lazy loading to reduce the size of their search page by 50%. This led to a 30% improvement in page load times.

These real-world examples demonstrate the tangible impact that code splitting can have on application performance and user experience.

Conclusion

In today‘s web landscape, application performance is no longer a nice-to-have; it‘s a necessity. Users expect fast, responsive experiences, and developers need to use every tool at their disposal to meet those expectations.

Code splitting with Webpack is one such tool. By dividing your application code into multiple chunks and loading those chunks on-demand, you can significantly improve initial load times, caching efficiency, and overall performance.

Whether you‘re building a small vanilla JavaScript application or a large-scale single-page app, code splitting is a technique every web developer should have in their toolkit.

Further Reading

If you want to dive deeper into code splitting and Webpack optimization, here are a few resources I recommend:

Happy splitting!

Similar Posts