前端5月28日 03:28
Webpack 有哪些优化手段?Webpack 有哪些优化手段?
从构建速度和产物体积两个方向回答,面试中最常考察的优化点如下:
## 构建速度优化
### 1. 持久化缓存
```js
// webpack.config.js
module.exports = {
cache: {
type: 'filesystem', // Webpack 5 支持
buildDependencies: {
config: [__filename]
}
}
}
```
首次构建后缓存写入 `node_modules/.cache/webpack`,二次构建可减少 60%-80% 时间。修改配置文件会自动失效。
### 2. 缩小 Loader 处理范围
```js
module.exports = {
module: {
rules: [{
test: /\.js$/,
include: path.resolve(__dirname, 'src'), // 只处理 src 目录
exclude: /node_modules/,
use: 'babel-loader'
}]
}
}
```
不配置 `include` 时,Babel 会遍历 `node_modules` 中所有 JS 文件,构建时间翻倍。
### 3. 多线程编译
- `thread-loader`:将耗时的 loader 放到 worker 池中并行执行
- `terser-webpack-plugin` 开启 `parallel: true`:并行压缩 JS
- Webpack 5 原生支持 `parallel` 配置
### 4. 优化模块解析
```js
module.exports = {
resolve: {
extensions: ['.ts', '.tsx', '.js'], // 按使用频率排列,减少尝试次数
alias: { '@': path.resolve(__dirname, 'src') }, // 别名减少路径查找
},
module: {
noParse: /lodash|jquery/, // 跳过已打包库的解析
}
}
```
`noParse` 告诉 Webpack 这些库没有 `import/require`,不需要解析依赖关系。
### 5. 使用更快的编译器
- `swc-loader` 替代 `babel-loader`(Rust 实现,速度快 20 倍以上)
- `esbuild-loader` 替代 `terser-webpack-plugin`(Go 实现)
### 6. DLL 预编译(Webpack 4 适用)
```js
// webpack.dll.config.js
new webpack.DllPlugin({
name: '[name]',
path: path.join(__dirname, 'dll', '[name]-manifest.json')
})
```
将 React、Vue 等不变库提前编译成 DLL,开发构建跳过。Webpack 5 的持久化缓存已基本替代此方案。
## 产物体积优化
### 1. 代码分割(Code Splitting)
**splitChunks — 提取公共依赖:**
```js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
```
将 `node_modules` 中的库打包到独立 chunk,利用浏览器缓存。
**动态 import() — 按需加载:**
```js
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
```
路由级懒加载,首屏只加载当前路由代码。
> splitChunks 是配置层面的自动提取,动态 import() 是代码层面的手动分割,两者配合使用。
### 2. Tree Shaking
```js
module.exports = {
mode: 'production', // 自动开启
optimization: {
usedExports: true,
minimize: true,
}
}
```
**前提条件:**
- 必须使用 ESM(`import/export`),不能用 CommonJS(`require`)
- `babel-loader` 配置 `"modules": false` 防止转译为 CJS
- `package.json` 标记 `"sideEffects": false` 告知安全移除未使用导出
**为什么需要 ESM?** ESM 的 `import/export` 是静态声明,编译时可确定依赖关系;CommonJS 的 `require` 是运行时调用,可以在 `if` 中使用,打包工具无法静态分析。
### 3. Scope Hoisting(作用域提升)
```js
module.exports = {
optimization: {
concatenateModules: true, // 生产模式默认开启
}
}
```
将模块合并到同一个闭包中,减少函数声明和闭包数量,体积可减小 5%-10%。
### 4. 压缩
- JS:`terser-webpack-plugin`(Webpack 5 生产模式默认启用)
- CSS:`css-minimizer-webpack-plugin`
```js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
}
}
```
### 5. externals 排除大型库
```js
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
```
将 React、Lodash 等通过 CDN 引入,不打包进 bundle,显著减小产物体积。
### 6. 图片与资源优化
- 小于 8KB 的图片转 base64 内联(Webpack 5 的 `asset/inline`)
- 大图使用 `image-webpack-loader` 压缩或转 WebP
```js
{
test: /\.(png|jpg)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 } // 8KB 以下内联
}
}
```
### 7. IgnorePlugin 排除无用模块
```js
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/
})
```
忽略 moment.js 的 locale 文件,体积减少约 200KB。
## 构建分析工具
| 工具 | 用途 |
|------|------|
| `webpack-bundle-analyzer` | 可视化分析产物体积,找出大依赖 |
| `speed-measure-webpack-plugin` | 统计各 loader/plugin 耗时 |
| `webpack --profile --json > stats.json` | 导出构建统计数据 |
## Webpack 5 新特性补充
- **Module Federation**:跨应用共享模块,实现微前端,避免重复打包公共依赖
- **持久化缓存**:替代 DLL 方案,配置更简单
- **Asset Modules**:内置资源模块,替代 `url-loader`/`file-loader`
- **更好的 Tree Shaking**:支持嵌套的 Tree Shaking 和内部模块的 Tree Shaking
## 追问
### splitChunks 和动态 import 有什么区别?
`splitChunks` 是 Webpack 配置层面的自动优化,根据策略提取公共依赖(如 `node_modules` 中的库),开发者无需手动干预。动态 `import()` 是代码层面的手动分割,由开发者决定哪些模块按需加载。两者互补:splitChunks 处理共享依赖,动态 import 处理路由级懒加载。
### Tree-Shaking 为什么需要 ESM?
ESModule 的 `import/export` 是静态声明,在编译阶段就能确定模块的依赖关系和哪些导出被使用。CommonJS 的 `require` 是运行时调用,可以写在 `if` 语句、循环或函数中,打包工具无法在编译时判断哪些代码会被执行,因此无法安全移除未使用的代码。此外,需确保 Babel 不将 ESM 转译为 CJS(配置 `"modules": false`),并在 `package.json` 中声明 `"sideEffects": false`。
### webpack 的 cache 缓存了什么?为什么能加快构建?
缓存了每个模块的 loader 处理结果和编译产物。首次构建后写入磁盘(`node_modules/.cache/webpack`),二次构建时 Webpack 检查文件时间戳和内容 hash,只重新编译变化的模块及其依赖链上的模块,未变化的模块直接使用缓存。典型场景:修改一个组件文件,只重新编译该文件和引用它的文件,其余数千模块跳过,构建时间从 30s 降到 5s 以内。Webpack 5 的 `filesystem` 缓存相比 Webpack 4 的 `memory` 缓存,重启开发服务器后缓存不丢失。
### Module Federation 解决了什么问题?
多个独立构建的应用之间共享模块,避免重复打包相同依赖。典型场景:微前端架构中,主应用和子应用都依赖 React,传统方案各自打包 React,用 Module Federation 可以在运行时共享一份 React,减少总体加载量。核心配置是 `exposes`(暴露模块)和 `remotes`(消费远程模块),实现应用间的模块级复用。标签
Webpack
Webpack是一个模块打包工具。它的主要目的是为了在浏览器中使用而打包JavaScript文件,但它也能转换、打包或打包几乎任何资源或资产。Webpack会取模块及其依赖关系,并生成代表这些模块的静态资源。Webpack支持扩展性,并在网络架构和性能方面推广最佳实践。

前端5月27日 01:10
webpack 的热更新原理是什么?详细聊聊热更新的流程HMR(Hot Module Replacement)的核心流程:
1. **webpack-dev-server 启动 WebSocket 服务**,和浏览器建立长连接
2. 文件变化 → webpack 重新编译 → 生成两个文件:`[hash].hot-update.json`(变更模块清单)和 `[hash].hot-update.js`(变更模块代码)
3. 通过 WebSocket 推送 hash 事件给浏览器
4. 浏览器用 AJAX 拉 `hot-update.json`,用 JSONP 拉 `hot-update.js`
5. HMR runtime 找到变更的模块,执行 `module.hot.accept` 注册的回调
6. 如果模块没有注册 accept,HMR runtime 向上冒泡,直到找到 accept 或刷新整个页面
Vue-loader、style-loader 等 loader 内部自动调用了 `module.hot.accept`,所以改 .vue 文件能无刷新更新。
## 追问
### 为什么需要两步拉取(json + js)?
`hot-update.json` 先确认哪些模块变了,如果当前页面没有引用变更模块,就不需要拉 `hot-update.js`。另外 json 由 webpack-dev-server 直接提供(内存文件系统),js 则走 JSONP 来避免跨域问题。
### React Fast Refresh 和普通 HMR 有什么区别?
React Fast Refresh 在 HMR 基础上做了 React 组件的"签名"匹配——换掉的组件如果有相同的 hooks 调用和组件的导出签名,就保留下面的组件状态(state 不丢失)。普通 HMR 只是替换模块代码,状态会重置。
### HMR 失效了怎么排查?
- `module.hot.accept` 是否注册(检查对应 loader 是否配置正确)
- 文件路径大小写不一致(macOS 不敏感但 webpack 敏感)
- 配置文件中 `hot: true` 和 `HotModuleReplacementPlugin` 是否启用
前端2024年8月5日 12:48
Webpack 的详细工作流程Webpack是一个现代JavaScript应用程序的静态模块打包器,它主要的工作就是分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(如TypeScript等),并将其转换和打包为合适的格式供浏览器使用。
Webpack的工作流程主要分为以下几个阶段:
### 初始化
在Webpack启动后,它会从配置文件(默认是 `webpack.config.js`)中读取配置的参数,合并命令行传递过来的参数,形成最终的配置对象。
### 编译(Compiling)
Webpack开始编译整个项目。在这个阶段,Webpack会根据配置中的入口(Entry)开始递归解析所有依赖项。配置文件中的 `entry`属性用于定义入口文件,可能是一个或多个。
### 构建(Building)
针对每个依赖项,Webpack会使用对应的loader去处理文件,例如使用 `babel-loader`来处理JavaScript文件,`css-loader`处理CSS文件,`file-loader`处理图片等资源。Loaders的定义让Webpack能够去处理非JavaScript文件(Webpack本身只理解JavaScript)。
### 生成(Output)
经过加载和转换,Webpack会根据配置中的 `output`部分,把处理过的文件生成到文件系统中。通常是在项目的 `dist`目录下生成 `bundle.js`或者其他自定义名称的文件。
### 优化(Optimizing)
在生成出来的文件中,Webpack可以进行代码压缩、分割代码以实现按需加载等优化操作。这通过配置 `plugins`来实现,比如 `UglifyJsPlugin`、`SplitChunksPlugin`等。
### 输出(Emitting)
将所有的资源文件输出到指定目录下,此时,Webpack的工作就算是完成了。
### 示例
例如,您有一个项目,其入口文件是 `src/index.js`。Webpack会解析这个文件,并解析出这个文件依赖的模块。假如 `index.js`中依赖了 `src/print.js`,Webpack会继续解析 `print.js`的依赖。
假设 `index.js`中还使用了ES6的语法和 `.scss`样式文件,那么在构建阶段,Webpack会使用 `babel-loader`将ES6代码转换为ES5代码,使用 `sass-loader`将SCSS文件转换为CSS文件,并且结合 `css-loader`和 `style-loader`将CSS代码注入到JavaScript中,这样就可以通过JavaScript将样式添加到DOM上。
在优化阶段,可能会有插件去检查代码,去除未引用的代码(dead code),压缩混淆输出的文件,以减少文件大小和提高加载速度。
最终,在输出阶段,Webpack会在 `dist`目录下输出 `bundle.js`,其中包含了所有的应用程序代码,以及所有的样式打包成的CSS代码。
这就是Webpack的一个基本工作流程。它的强大之处在于可扩展性,通过配置文件和插件系统,可以适应各种复杂的项目需求。
前端2024年6月24日 16:43
webpack 的 loader 的作用是什么?如何自定义一个loader?### webpack 的 loader 的作用是什么?
webpack 的 loader 有着非常关键的作用。在 webpack 打包过程中,loader 负责处理源代码文件,并且将其转换成 webpack 能够处理的模块。由于 webpack 本身只理解 JavaScript,所以当我们需要在 JavaScript 代码中导入 CSS、图片或者其他非 JavaScript 文件时,就需要用到 loader。
例如,如果我们想在 JavaScript 中导入一个 CSS 文件,我们会使用 `style-loader` 和 `css-loader`。这两个 loader 分别做两件事情:
1. `css-loader` 会处理 `import` 和 `url()` 等,就像 JavaScript 模块一样处理 CSS 文件。
2. `style-loader` 会将计算后的 CSS 注入到页面的 `<style>` 标签中。
其他常用的 loader 还包括:
- `babel-loader`:将 ES6+ 代码转换成兼容性更好的 JavaScript 代码。
- `file-loader`:处理文件导入,将文件输出到输出目录,并返回文件的 URL。
- `url-loader`:像 `file-loader` 但在文件大小低于指定的限制时会返回 Data URL。
- `sass-loader`:将 SASS/SCSS 文件编译成 CSS。
- 等等。
Loader 可以链式调用,每个 loader 将上一个处理的结果传递给下一个 loader,直到最后输出 webpack 能够处理的 JavaScript 模块。
### 如何自定义一个 loader?
自定义一个 loader 需要遵循一定的步骤:
1. **创建一个 JavaScript 函数**:
- 这个函数将接收源文件的内容作为参数。
- 函数内部,可以使用任何 JavaScript 代码来处理输入的内容。
- 函数最后需要返回处理后的内容,或者使用 `this.callback` 方法异步返回处理结果和可能的 map。
2. **遵守 loader 的规则**:
- 接收源文件的内容作为参数。
- 不修改原始内容,返回一个新的字符串或者 Buffer。
- 在需要时,可以通过 `this.async` 返回一个异步函数。
- 使用 `this.cacheable` 标记 loader 的结果是否可缓存。
- 通过 `this.loaders` 访问配置中的其他 loaders。
3. **导出这个函数**:
- 使用 `module.exports` 导出这个 loader 函数。
4. **在 webpack 配置中使用你的 loader**:
- 在 `module.rules` 中添加一个规则来使用你的 loader。
例子:
假设我们要创建一个简单的 markdown loader,将 markdown 转换为 HTML。
```javascript
const marked = require('marked');
module.exports = function (markdown) {
// 将 markdown 转换成 HTML
const html = marked(markdown);
// 返回 HTML 字符串
return `module.exports = ${JSON.stringify(html)}`;
};
```
然后在 webpack 配置文件中使用这个 loader:
```javascript
module.exports = {
// ...
module: {
rules: [
{
test: /\.md$/,
use: [
{ loader: 'html-loader' },
{ loader: path.resolve('./path/to/your/custom-markdown-loader.js') }
]
}
]
}
// ...
};
```
注意,这里我们使用了 `html-loader` 来处理我们自定义 loader 输出的 HTML 字符串,这样做可以确保 HTML 字符串也遵循 webpack 的模块系统。
通过这种方式,开发者可以根据项目需求创建特定功能的 loader,以适应各种类型的文件处理场景。