DEV Community

Tianya School
Tianya School

Posted on

Webpack Code Splitting Dynamic and Lazy Loading

Let’s dive into Webpack’s Code Splitting, a powerful tool for frontend performance optimization, especially in large projects. Code splitting, through dynamic and lazy loading, breaks large JavaScript bundles into smaller chunks, loading them on-demand to reduce initial load times and enhance user experience. We’ll cover the principles, usage, and real-world scenarios, with React and Vue examples, guiding you step-by-step to implement dynamic and lazy loading. Expect practical details to supercharge your page load speed!

Why Code Splitting?

Modern frontend projects are complex, with React, Vue, and libraries often producing multi-MB JavaScript bundles, slowing initial loads. Google research shows that over 40% of users abandon pages taking longer than 3 seconds to load. Code splitting addresses this by dividing code into smaller chunks, loading only what’s needed for the current page and fetching others on-demand. Key benefits include:

  • Reduced Initial Load: Loads only core code, cutting first-paint time.
  • On-Demand Loading: Fetches code for specific features when triggered.
  • Cache Efficiency: Smaller chunks mean browsers only re-download changed parts.

Webpack’s code splitting leverages dynamic import() and lazy loading. We’ll start with basic configurations and progress to practical React and Vue applications.

Environment Setup

To use Webpack code splitting, set up the environment with Node.js (18.x recommended) and Webpack (version 5.x at the time of writing).

Create a project:

mkdir webpack-code-splitting
cd webpack-code-splitting
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
Enter fullscreen mode Exit fullscreen mode

Directory structure:

webpack-code-splitting/
├── src/
│   ├── index.js
│   ├── another-module.js
├── index.html
├── webpack.config.js
├── package.json
Enter fullscreen mode Exit fullscreen mode

Configure package.json:

{
  "scripts": {
    "build": "webpack",
    "start": "webpack serve"
  }
}
Enter fullscreen mode Exit fullscreen mode

Webpack Basics: Entry and Output

Start with a simple Webpack project to understand code splitting basics. Create webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js',
    clean: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'
    })
  ],
  devServer: {
    static: path.resolve(__dirname, 'dist'),
    port: 8080
  }
};
Enter fullscreen mode Exit fullscreen mode

index.html:

<!DOCTYPE html>
<html>
<head>
  <title>Webpack Code Splitting</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

src/index.js:

console.log('Hello from index.js!');
Enter fullscreen mode Exit fullscreen mode

src/another-module.js:

console.log('Hello from another module!');
Enter fullscreen mode Exit fullscreen mode

Run npm run build to generate dist/main.bundle.js. Run npm start and visit localhost:8080 to see “Hello from index.js!” in the console. This is a single-entry bundle; now let’s add code splitting.

Dynamic import(): On-Demand Loading

Webpack supports dynamic import() to split code into separate chunks, loaded on-demand. Update src/index.js:

document.getElementById('app').innerHTML = `
  <h1>Dynamic Import Demo</h1>
  <button id="loadBtn">Load Module</button>
`;

document.getElementById('loadBtn').addEventListener('click', async () => {
  const module = await import('./another-module.js');
  console.log(module);
});
Enter fullscreen mode Exit fullscreen mode

Run npm run build. The dist folder contains:

  • main.bundle.js: Main entry code.
  • 1.bundle.js: Dynamically loaded chunk (filename may vary).

Click the button, and the console prints “Hello from another module!”. The Network panel shows 1.bundle.js loads only on click, achieving lazy loading.

Magic Comments: Naming Chunks

Dynamic import() chunks are named numerically by default, which can be unclear. Use Webpack’s magic comments to name them:

document.getElementById('app').innerHTML = `
  <h1>Dynamic Import with Named Chunk</h1>
  <button id="loadBtn">Load Module</button>
`;

document.getElementById('loadBtn').addEventListener('click', async () => {
  const module = await import(/* webpackChunkName: "anotherModule" */ './another-module.js');
  console.log(module);
});
Enter fullscreen mode Exit fullscreen mode

Run npm run build to generate anotherModule.bundle.js, making the chunk name clearer.

Entry Splitting: Multi-Entry Configuration

Webpack also supports multi-entry splitting for multi-page applications (MPA). Update webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: {
    home: './src/home.js',
    about: './src/about.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js',
    clean: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'home.html',
      chunks: ['home']
    }),
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'about.html',
      chunks: ['about']
    })
  ],
  devServer: {
    static: path.resolve(__dirname, 'dist'),
    port: 8080
  }
};
Enter fullscreen mode Exit fullscreen mode

src/home.js:

document.getElementById('app').innerHTML = `
  <h1>Home Page</h1>
  <a href="/about.html">Go to About</a>
`;
Enter fullscreen mode Exit fullscreen mode

src/about.js:

document.getElementById('app').innerHTML = `
  <h1>About Page</h1>
  <a href="/home.html">Back to Home</a>
`;
Enter fullscreen mode Exit fullscreen mode

Run npm run build to generate home.bundle.js and about.bundle.js, corresponding to home.html and about.html. Visit localhost:8080/home.html to load only home.bundle.js; navigate to about.html to load about.bundle.js, achieving page-level splitting.

SplitChunksPlugin: Extracting Shared Code

In large projects, entries or dynamic modules often share code (e.g., lodash). Webpack’s SplitChunksPlugin extracts shared code to reduce duplication.

Update webpack.config.js:

module.exports = {
  mode: 'development',
  entry: {
    home: './src/home.js',
    about: './src/about.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js',
    clean: true
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'home.html',
      chunks: ['home', 'vendors']
    }),
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'about.html',
      chunks: ['about', 'vendors']
    })
  ],
  devServer: {
    static: path.resolve(__dirname, 'dist'),
    port: 8080
  }
};
Enter fullscreen mode Exit fullscreen mode

Install a shared library:

npm install lodash
Enter fullscreen mode Exit fullscreen mode

Update src/home.js and src/about.js:

// src/home.js
import _ from 'lodash';

document.getElementById('app').innerHTML = `
  <h1>Home Page</h1>
  <p>${_.join(['Hello', 'from', 'Home'], ' ')}</p>
  <a href="/about.html">Go to About</a>
`;
Enter fullscreen mode Exit fullscreen mode
// src/about.js
import _ from 'lodash';

document.getElementById('app').innerHTML = `
  <h1>About Page</h1>
  <p>${_.join(['Hello', 'from', 'About'], ' ')}</p>
  <a href="/home.html">Back to Home</a>
`;
Enter fullscreen mode Exit fullscreen mode

Run npm run build to generate:

  • home.bundle.js
  • about.bundle.js
  • vendors~home~about.bundle.js (contains lodash)

SplitChunksPlugin extracts lodash into a shared chunk, loaded once and cached by the browser.

React Lazy Loading: Dynamic Components

Use code splitting in a React project for component lazy loading. Create a React project:

npx create-react-app react-code-splitting
cd react-code-splitting
npm install @babel/plugin-syntax-dynamic-import
Enter fullscreen mode Exit fullscreen mode

Update package.json to add Babel plugin:

{
  "babel": {
    "presets": ["@babel/preset-react"],
    "plugins": ["@babel/plugin-syntax-dynamic-import"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Update src/App.js:

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import(/* webpackChunkName: "lazyComponent" */ './LazyComponent'));

function App() {
  return (
    <div style={{ padding: 20 }}>
      <h1>React Code Splitting</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/LazyComponent.js:

import React from 'react';

function LazyComponent() {
  return <p>This is a lazily loaded component!</p>;
}

export default LazyComponent;
Enter fullscreen mode Exit fullscreen mode

Run npm start. The initial load includes only App.js, and LazyComponent’s chunk loads on render. Suspense shows “Loading...” until the chunk is ready. The Network panel confirms lazyComponent.js loads lazily.

Route-Level Splitting

Use react-router-dom for route-based lazy loading:

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Update src/App.js:

import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

const Home = lazy(() => import(/* webpackChunkName: "home" */ './Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './About'));

function App() {
  return (
    <BrowserRouter>
      <div style={{ padding: 20 }}>
        <h1>React Router Splitting</h1>
        <nav>
          <Link to="/">Home</Link> | <Link to="/about">About</Link>
        </nav>
        <Suspense fallback={<p>Loading...</p>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Suspense>
      </div>
    </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/Home.js:

import React from 'react';

function Home() {
  return <h2>Home Page</h2>;
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

src/About.js:

import React from 'react';

function About() {
  return <h2>About Page</h2>;
}

export default About;
Enter fullscreen mode Exit fullscreen mode

Visit localhost:3000 to load home.js. Click “About” to load about.js. Suspense handles the loading UI, ensuring smooth route transitions.

Vue Lazy Loading: Dynamic Components

Use code splitting in a Vue project for component and route lazy loading. Create a Vue project:

npm install -g @vue/cli
vue create vue-code-splitting
cd vue-code-splitting
Enter fullscreen mode Exit fullscreen mode

Select default Vue 3 settings. Configure vue.config.js:

module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Dynamic Components

Update src/App.vue:

<template>
  <div style="padding: 20px;">
    <h1>Vue Code Splitting</h1>
    <button @click="showComponent = true">Load Component</button>
    <Suspense>
      <template #default>
        <LazyComponent v-if="showComponent" />
      </template>
      <template #fallback>
        <p>Loading...</p>
      </template>
    </Suspense>
  </div>
</template>

<script>
import { defineAsyncComponent, ref } from 'vue';

export default {
  components: {
    LazyComponent: defineAsyncComponent(() =>
      import(/* webpackChunkName: "lazyComponent" */ './components/LazyComponent.vue')
    )
  },
  setup() {
    const showComponent = ref(false);
    return { showComponent };
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

src/components/LazyComponent.vue:

<template>
  <p>This is a lazily loaded component!</p>
</template>
Enter fullscreen mode Exit fullscreen mode

Run npm run serve. Clicking the button loads lazyComponent.js, with Suspense showing “Loading...” during the process.

Route-Level Splitting

Use vue-router:

npm install vue-router@4
Enter fullscreen mode Exit fullscreen mode

Create src/router/index.js:

import { createRouter, createWebHistory } from 'vue-router';

const Home = () => import(/* webpackChunkName: "home" */ '../components/Home.vue');
const About = () => import(/* webpackChunkName: "about" */ '../components/About.vue');

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Update src/main.js:

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');
Enter fullscreen mode Exit fullscreen mode

Update src/App.vue:

<template>
  <div style="padding: 20px;">
    <h1>Vue Router Splitting</h1>
    <nav>
      <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link>
    </nav>
    <Suspense>
      <template #default>
        <router-view />
      </template>
      <template #fallback>
        <p>Loading...</p>
      </template>
    </Suspense>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

src/components/Home.vue:

<template>
  <h2>Home Page</h2>
</template>
Enter fullscreen mode Exit fullscreen mode

src/components/About.vue:

<template>
  <h2>About Page</h2>
</template>
Enter fullscreen mode Exit fullscreen mode

Visit localhost:8080 to load home.js. Navigate to /about to load about.js. SplitChunksPlugin extracts shared code (e.g., Vue core), reducing duplicate loads.

Lazy Loading Third-Party Libraries

Large projects often use third-party libraries (e.g., moment). Load them dynamically with import():

src/index.js:

document.getElementById('app').innerHTML = `
  <h1>Load Moment.js</h1>
  <button id="loadBtn">Load Moment</button>
  <p id="result"></p>
`;

document.getElementById('loadBtn').addEventListener('click', async () => {
  const moment = await import(/* webpackChunkName: "moment" */ 'moment');
  document.getElementById('result').innerText = moment.default().format('MMMM Do YYYY');
});
Enter fullscreen mode Exit fullscreen mode

Install moment:

npm install moment
Enter fullscreen mode Exit fullscreen mode

Run npm run build. moment.js is split into a separate chunk, loaded only on button click. The Network panel shows moment.js loads lazily, keeping the initial bundle light.

Performance Testing

Test code splitting with Chrome DevTools. Update src/App.js (React):

import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

const Home = lazy(() => import(/* webpackChunkName: "home" */ './Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './About'));

function App() {
  return (
    <BrowserRouter>
      <div style={{ padding: 20 }}>
        <h1>Performance Test</h1>
        <nav>
          <Link to="/">Home</Link> | <Link to="/about">About</Link>
        </nav>
        <Suspense fallback={<p>Loading...</p>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Suspense>
      </div>
    </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run npm run build && npx serve dist, then test with Lighthouse:

  • No Splitting: Single bundle ~1MB, initial load ~1.5s.
  • Code Splitting: Main bundle ~100KB, home.js ~50KB, FCP ~0.5s.

Lazy loading reduces subsequent page loads to ~200ms, with fast perceived performance.

Real-World Scenario: Large Single-Page Application

Create a React SPA with Home, Product List, and Product Detail pages:

src/App.js:

import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

const Home = lazy(() => import(/* webpackChunkName: "home" */ './Home'));
const ProductList = lazy(() => import(/* webpackChunkName: "productList" */ './ProductList'));
const ProductDetail = lazy(() => import(/* webpackChunkName: "productDetail" */ './ProductDetail'));

function App() {
  return (
    <BrowserRouter>
      <div style={{ padding: 20 }}>
        <h1>E-commerce SPA</h1>
        <nav>
          <Link to="/">Home</Link> | <Link to="/products">Products</Link>
        </nav>
        <Suspense fallback={<p>Loading...</p>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/products" element={<ProductList />} />
            <Route path="/products/:id" element={<ProductDetail />} />
          </Routes>
        </Suspense>
      </div>
    </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/Home.js:

import React from 'react';

function Home() {
  return <h2>Welcome to our store!</h2>;
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

src/ProductList.js:

import React from 'react';
import { Link } from 'react-router-dom';

const products = [
  { id: 1, name: 'Product 1' },
  { id: 2, name: 'Product 2' }
];

function ProductList() {
  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <Link to={`/products/${product.id}`}>{product.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

src/ProductDetail.js:

import React from 'react';
import { useParams } from 'react-router-dom';

function ProductDetail() {
  const { id } = useParams();
  return <h2>Product {id} Details</h2>;
}

export default ProductDetail;
Enter fullscreen mode Exit fullscreen mode

Run the app. Visiting / loads only home.js. Clicking “Products” loads productList.js, and product links load productDetail.js. Each chunk loads on-demand, keeping the initial load fast.

Top comments (0)