返回首页

代码分割的本质是什么?有什么意义呢?

问题解析

代码分割(Code Splitting)是 Webpack 的核心特性之一,它解决了源代码直接上线和打包成唯一脚本两种极端方案的问题,在服务器性能和用户体验之间找到平衡点。

核心概念

三种部署方案的对比

方案 优点 缺点
源代码直接上线 按需加载,无冗余 HTTP 请求多,性能开销大
打包成唯一脚本 请求少,缓存友好 页面空白期长,首屏慢
代码分割 平衡两者优势 需要合理配置

代码分割的本质

代码分割 = 源代码直接上线 和 打包成唯一脚本 之间的中间状态

本质:用可接受的服务器性能压力增加换取更好的用户体验

详细解答

方案对比详解

方案 1:源代码直接上线

项目结构:
src/
  ├── utils/
  │   ├── helper.js      -> HTTP 请求 1
  │   ├── format.js      -> HTTP 请求 2
  │   └── validate.js    -> HTTP 请求 3
  ├── components/
  │   ├── Button.jsx     -> HTTP 请求 4
  │   ├── Input.jsx      -> HTTP 请求 5
  │   └── Modal.jsx      -> HTTP 请求 6
  └── pages/
      ├── Home.jsx       -> HTTP 请求 7
      └── About.jsx      -> HTTP 请求 8

问题:
- 8 个 HTTP 请求,建立连接开销大
- 浏览器并发请求数限制(通常 6-8 个)
- 请求排队,加载时间延长

方案 2:打包成唯一脚本

// webpack.config.js
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'  // 所有代码打包成一个文件
  }
};
问题:
- 文件体积巨大(可能几 MB)
- 首屏加载时间极长
- 页面空白期严重影响用户体验
- 修改一行代码,整个缓存失效

方案 3:代码分割(推荐)

// webpack.config.js
module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  },
  output: {
    filename: '[name].[chunkhash:8].js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};
优势:
- 合理的文件数量(通常 3-10 个)
- 首屏加载快,按需加载其他代码
- 缓存利用率高,公共代码单独打包
- 并行加载,充分利用浏览器并发

代码分割的实现方式

1. 入口分割(Entry Splitting)

module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js'  // 独立的入口
  },
  output: {
    filename: '[name].js'
  }
};

2. 动态导入(Dynamic Import)

// 路由懒加载
const Home = () => import(/* webpackChunkName: "home" */ './pages/Home');
const About = () => import(/* webpackChunkName: "about" */ './pages/About');

// 条件加载
if (needChart) {
  const Chart = await import(/* webpackChunkName: "chart" */ './components/Chart');
}

// 事件触发加载
button.addEventListener('click', () => {
  import(/* webpackChunkName: "modal" */ './components/Modal')
    .then(module => {
      module.showModal();
    });
});

3. SplitChunksPlugin

module.exports = {
  optimization: {
    splitChunks: {
      // 选择哪些 chunk 进行分割
      chunks: 'all',  // async(异步), initial(同步), all(全部)

      // 最小分割大小(字节)
      minSize: 20000,  // 20KB

      // 最大分割大小
      maxSize: 0,

      // 最小被引用次数
      minChunks: 1,

      // 最大异步请求数
      maxAsyncRequests: 30,

      // 最大初始请求数
      maxInitialRequests: 30,

      // 缓存组
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10  // 优先级
        },

        // 公共代码
        common: {
          minChunks: 2,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

深入理解

代码分割的加载流程

用户访问页面
    |
    v
加载 HTML + 关键 CSS/JS (首屏)
    |
    v
页面可交互(首屏渲染完成)
    |
    v
按需加载其他模块(懒加载)
    |
    v
预加载后续可能需要的模块(预加载)

预加载和预获取

// 预获取:可能在将来需要(导航到其他页面)
import(/* webpackPrefetch: true */ './pages/About');

// 预加载:当前页面即将需要
import(/* webpackPreload: true */ './components/Chart');
<!-- prefetch:在浏览器空闲时加载 -->
<link rel="prefetch" href="about.js">

<!-- preload:立即加载,优先级高 -->
<link rel="preload" href="chart.js" as="script">

代码分割与缓存策略

module.exports = {
  output: {
    // 使用 contenthash 确保缓存有效
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js'
  },

  optimization: {
    splitChunks: {
      cacheGroups: {
        // 第三方库单独打包,缓存时间长
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // 获取包名
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            return `npm.${packageName.replace('@', '')}`;
          }
        }
      }
    },

    // 运行时代码单独打包
    runtimeChunk: {
      name: 'runtime'
    }
  }
};

最佳实践

React 路由代码分割

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

// 懒加载页面组件
const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));
const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Vue 路由代码分割

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
];

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

完整的代码分割配置

const path = require('path');

module.exports = {
  entry: {
    main: './src/index.js'
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    clean: true
  },

  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,  // 244KB,超过则尝试分割
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,

      cacheGroups: {
        // React 相关库
        reactVendor: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react-vendor',
          chunks: 'all',
          priority: 20
        },

        // UI 组件库
        uiVendor: {
          test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
          name: 'ui-vendor',
          chunks: 'all',
          priority: 15
        },

        // 工具库
        utilsVendor: {
          test: /[\\/]node_modules[\\/](lodash|moment|axios)[\\/]/,
          name: 'utils-vendor',
          chunks: 'all',
          priority: 10
        },

        // 其他第三方库
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        },

        // 公共代码
        common: {
          minChunks: 2,
          chunks: 'all',
          enforce: true,
          priority: 1
        }
      }
    },

    // 运行时代码单独打包
    runtimeChunk: {
      name: 'runtime'
    }
  }
};

性能监控指标

// 监控代码分割效果
window.addEventListener('load', () => {
  // 首屏加载时间
  const timing = performance.timing;
  const loadTime = timing.loadEventEnd - timing.navigationStart;
  console.log(`Page load time: ${loadTime}ms`);

  // 资源加载情况
  const resources = performance.getEntriesByType('resource');
  const jsResources = resources.filter(r => r.name.endsWith('.js'));
  console.log(`JS files loaded: ${jsResources.length}`);
  console.log(`Total JS size: ${jsResources.reduce((sum, r) => sum + r.transferSize, 0)} bytes`);
});

面试要点

  1. 代码分割的本质:源代码直接上线和打包成唯一脚本之间的中间状态,用可接受的服务器性能压力换取更好的用户体验

  2. 三种方案的对比

    • 源代码直接上线:HTTP 请求多,性能开销大
    • 打包成唯一脚本:页面空白期长,用户体验不好
    • 代码分割:平衡两者,合理分割文件
  3. 实现方式

    • 入口分割(Entry Splitting)
    • 动态导入(Dynamic Import)
    • SplitChunksPlugin
  4. SplitChunksPlugin 配置

    • chunks: 选择哪些 chunk 进行分割
    • minSize: 最小分割大小
    • cacheGroups: 缓存组配置
  5. 预加载策略

    • webpackPrefetch: 预获取,空闲时加载
    • webpackPreload: 预加载,高优先级加载
  6. 最佳实践

    • 路由懒加载
    • 第三方库单独打包
    • 运行时代码单独打包
    • 使用 contenthash 优化缓存