MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Webpack 配置文件的优化技巧

2021-05-227.9k 阅读

理解 Webpack 配置文件基础

Webpack 是一个流行的前端模块打包工具,它通过一个配置文件(通常是 webpack.config.js)来定义如何处理项目中的各种资源。在深入优化之前,先确保对基础配置有清晰理解。

入口(entry)

入口指示 Webpack 应该从哪个模块开始构建其内部依赖图。常见的配置方式如下:

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

这里指定了项目的入口文件为 src/index.js。如果项目有多个入口,比如一个用于前端页面,一个用于后端服务,可以这样配置:

module.exports = {
  entry: {
    app: './src/client/index.js',
    server: './src/server/index.js'
  }
};

输出(output)

输出告诉 Webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。基本配置如下:

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

path 是输出目录的绝对路径,这里使用 path.resolve 来获取 dist 目录的绝对路径。filename 定义了输出文件的名称。如果有多个入口,可以使用占位符来命名输出文件:

module.exports = {
  entry: {
    app: './src/client/index.js',
    server: './src/server/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js'
  }
};

这样,app 入口对应的输出文件就是 app.bundle.jsserver 入口对应的是 server.bundle.js

模块(module)

Webpack 本身只能理解 JavaScript 和 JSON 文件,对于其他类型的文件,比如 CSS、图片等,需要使用 loader 来处理。module 配置项用于定义这些 loader。例如,处理 CSS 文件的配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
           'css-loader'
        ]
      }
    ]
  }
};

test 用于匹配文件路径,这里匹配所有 .css 文件。use 数组指定了处理 CSS 文件的 loader,从右到左(从下到上)执行,先使用 css-loader 解析 CSS 文件,再使用 style-loader 将 CSS 插入到 DOM 中。

解析(resolve)

resolve 配置项帮助 Webpack 找到需要引入的模块。比如,配置别名可以让导入路径更简洁:

module.exports = {
  resolve: {
    alias: {
      '@src': path.resolve(__dirname,'src')
    }
  }
};

这样在代码中就可以使用 import { someFunction } from '@src/utils'; 来导入 src/utils 下的模块,而不需要写冗长的相对路径。

优化 Webpack 配置文件的策略

优化构建速度

  1. 减少 loader 作用范围
    • 当配置 loader 时,通过 includeexclude 选项来精确指定 loader 作用的文件范围,可以显著提高构建速度。例如,对于 Babel-loader,只对 src 目录下的文件进行转译:
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset - env']
          }
        }
      }
    ]
  }
};
  • exclude: /node_modules/ 表示不处理 node_modules 中的文件,因为这些文件通常已经是经过编译或处理好的,不需要再次通过 Babel 转译,从而节省了大量的构建时间。
  1. 使用 HappyPack
    • HappyPack 可以将 loader 的执行由单线程转换为多线程,利用多核 CPU 的优势来加速构建。首先安装 happy - packnpm install happy - pack --save - dev
    • 然后修改 Webpack 配置:
const HappyPack = require('happy - pack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'HappyPack/loader?id=js'
      }
    ]
  },
  plugins: [
    new HappyPack({
      id: 'js',
      threadPool: happyThreadPool,
      loaders: [
        {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset - env']
          }
        }
      ]
    })
  ]
};
  • 这里将 Babel - loader 的执行交给 HappyPack 管理,id 用于标识该 HappyPack 实例,threadPool 配置了线程池大小,根据 CPU 核心数来设置可以充分利用系统资源,加快构建速度。
  1. 使用 DllPlugin 和 DllReferencePlugin
    • 这两个插件可以将一些不经常变动的第三方库提前打包,在后续构建中直接引用,而不需要每次都重新打包这些库。
    • 首先,创建一个单独的 Webpack 配置文件(例如 webpack.dll.js)来打包第三方库:
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react','react - dom']
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name].manifest.json'),
      name: '[name]_library'
    })
  ]
};
  • 运行 webpack --config webpack.dll.js 生成 vendor.dll.jsvendor.manifest.json
  • 然后在主 Webpack 配置文件中使用 DllReferencePlugin
const path = require('path');
const webpack = require('webpack');

module.exports = {
  //...其他配置
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, 'dll', 'vendor.manifest.json')
    })
  ]
};
  • 这样在每次构建时,Webpack 会直接引用 vendor.dll.js 中的库,而不需要重新打包 reactreact - dom,大大加快了构建速度。

优化输出文件体积

  1. 代码压缩
    • 使用 UglifyJSPlugin 对 JavaScript 文件进行压缩。Webpack 4 及以上版本默认使用 terser - webpack - plugin 进行代码压缩。可以通过以下方式配置:
const TerserPlugin = require('terser - webpack - plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true
          }
        }
      })
    ]
  }
};
  • parallel: true 开启并行压缩,加快压缩速度。drop_console: true 会删除代码中的 console.log 等语句,进一步减小文件体积。
  1. Tree - shaking
    • Tree - shaking 是一种通过分析代码中的导入和导出,去除未使用代码的优化策略。要启用 Tree - shaking,项目必须使用 ES6 模块语法(importexport),并且 Webpack 配置中需要设置 mode'production',因为在生产模式下,Webpack 会自动启用 Tree - shaking。
    • 例如,有如下代码结构:
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// main.js
import { add } from './utils';

console.log(add(2, 3));
  • 在生产模式下,Webpack 会分析 main.js 只导入了 add 函数,因此会去除 subtract 函数相关的代码,减小输出文件体积。
  1. 分离 CSS
    • 对于 CSS 文件,使用 MiniCssExtractPlugin 可以将 CSS 从 JavaScript 中分离出来,形成单独的 CSS 文件。首先安装 mini - css - extract - pluginnpm install mini - css - extract - plugin --save - dev
    • 然后修改 Webpack 配置:
const MiniCssExtractPlugin = require('mini - css - extract - plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css'
    })
  ]
};
  • 这样会在输出目录中生成单独的 CSS 文件,而不是将 CSS 嵌入到 JavaScript 中,有利于浏览器并行加载资源,提高页面加载速度,并且减小了 JavaScript 文件的体积。

优化开发体验

  1. 热模块替换(HMR)
    • HMR 允许在应用程序运行时更新模块,而无需重新加载整个页面。在 Webpack 中启用 HMR 很简单,只需要在开发服务器配置中添加 hot: true。例如,使用 webpack - dev - server
module.exports = {
  //...其他配置
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    hot: true
  }
};
  • 同时,对于某些模块,需要添加一些代码来处理 HMR。以 React 应用为例,可以使用 react - hot - loader
import React from'react';
import ReactDOM from'react - dom';
import App from './App';

const rootElement = document.getElementById('root');

const render = () => {
  ReactDOM.render(<App />, rootElement);
};

render();

if (module.hot) {
  module.hot.accept('./App', () => {
    render();
  });
}
  • 这里 module.hot.accept 监听 App.js 的变化,当 App.js 发生改变时,重新渲染页面,而不需要刷新整个页面,大大提高了开发效率。
  1. Source Map
    • Source Map 可以将编译后的代码映射回原始源代码,方便调试。Webpack 提供了多种 Source Map 模式,可以根据需求选择。例如,在开发环境中,可以使用 eval - source - map,它生成速度快,并且可以提供较好的调试体验:
module.exports = {
  //...其他配置
  devtool: 'eval - source - map'
};
  • 在生产环境中,为了减小文件体积,可以使用 source - map,它会生成一个单独的 .map 文件,但不会影响主文件的体积:
module.exports = {
  //...其他配置
  devtool:'source - map'
};
  • 不同的 Source Map 模式在生成速度、文件体积和调试准确性上有所不同,需要根据实际情况选择。

针对不同环境的优化配置

开发环境优化

  1. 快速构建
    • 在开发环境中,构建速度是关键。除了前面提到的减少 loader 作用范围、使用 HappyPack 等方法外,还可以使用 webpack - dev - serverwatchOptions 来优化文件监听。例如:
module.exports = {
  //...其他配置
  devServer: {
    //...其他配置
    watchOptions: {
      ignored: /node_modules/,
      poll: 1000
    }
  }
};
  • ignored: /node_modules/ 表示不监听 node_modules 中的文件变化,减少不必要的监听开销。poll: 1000 表示每 1000 毫秒轮询一次文件系统变化,对于某些不支持文件系统监听的系统(如网络文件系统),可以使用轮询方式。
  1. 增强调试信息
    • 除了使用 Source Map 外,还可以在 Webpack 配置中添加一些插件来增强调试信息。例如,webpack - bundle - analyzer 插件可以生成可视化的 bundle 分析报告,帮助了解 bundle 的组成和大小。首先安装 webpack - bundle - analyzernpm install webpack - bundle - analyzer --save - dev
    • 然后在 Webpack 配置中添加插件:
const BundleAnalyzerPlugin = require('webpack - bundle - analyzer').BundleAnalyzerPlugin;

module.exports = {
  //...其他配置
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};
  • 运行 Webpack 构建后,会自动打开一个浏览器窗口,显示 bundle 的分析报告,包括各个模块的大小、依赖关系等,有助于发现体积过大的模块并进行优化。

生产环境优化

  1. 极致的代码压缩
    • 在生产环境中,除了使用 TerserPlugin 进行 JavaScript 代码压缩外,还可以对 CSS 和 HTML 文件进行压缩。对于 CSS,可以使用 OptimizeCSSAssetsPlugin。首先安装 optimize - css - assets - pluginnpm install optimize - css - assets - plugin --save - dev
    • 然后在 Webpack 配置中添加到 optimization.minimizer 数组中:
const OptimizeCSSAssetsPlugin = require('optimize - css - assets - plugin');
const TerserPlugin = require('terser - webpack - plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true
          }
        }
      }),
      new OptimizeCSSAssetsPlugin({})
    ]
  }
};
  • 对于 HTML 文件,可以使用 html - webpack - pluginminify 选项进行压缩。例如:
const HtmlWebpackPlugin = require('html - webpack - plugin');

module.exports = {
  //...其他配置
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    })
  ]
};
  • collapseWhitespace 会移除 HTML 中的空白字符,removeComments 会移除 HTML 中的注释,从而减小 HTML 文件的体积。
  1. CDN 集成
    • 在生产环境中,将静态资源(如 JavaScript、CSS、图片等)部署到 CDN 可以提高资源的加载速度。可以使用 html - webpack - cdn - plugin 来实现。首先安装 html - webpack - cdn - pluginnpm install html - webpack - cdn - plugin --save - dev
    • 然后在 Webpack 配置中添加插件:
const HtmlWebpackCDNPlugin = require('html - webpack - cdn - plugin');

module.exports = {
  //...其他配置
  plugins: [
    new HtmlWebpackCDNPlugin({
      head: [
        {
          src: 'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js',
          integrity: 'sha512 - 92T892391FZ06442p88D8Z776cY257c52+8z52YfWZpQc+3p1287G5eG04860Yc89W9n8J95k+5991409f15274g==',
          crossorigin: 'anonymous'
        },
        {
          src: 'https://cdnjs.cloudflare.com/ajax/libs/react - dom/17.0.2/umd/react - dom.production.min.js',
          integrity: 'sha512 - hn458424769V0W1fZ2C7032t3XjWm095H34YVv66j8j29n071Xb7aZ59W9eX8z4K9WZ2D9V88J80e3b9k7K8e+2A==',
          crossorigin: 'anonymous'
        }
      ]
    })
  ]
};
  • 这里通过 html - webpack - cdn - plugin 将 React 和 React - DOM 的生产版本从 CDN 引入,而不是打包到本地,从而减小了本地 bundle 的体积,并且利用 CDN 的全球分布节点加速资源加载。

动态导入与代码分割优化

动态导入的原理与使用

  1. 动态导入语法
    • 在 ES2020 中,引入了动态导入(Dynamic Imports)语法,它允许在运行时按需导入模块。在 Webpack 中,动态导入会自动触发代码分割。例如:
// main.js
document.getElementById('loadButton').addEventListener('click', async () => {
  const { someFunction } = await import('./utils.js');
  someFunction();
});
  • 这里当用户点击 loadButton 时,才会异步导入 utils.js 模块,并调用其中的 someFunction
  1. Webpack 对动态导入的处理
    • Webpack 会将动态导入的模块单独打包成一个 chunk。在构建时,Webpack 会分析代码中的动态导入语句,并为每个动态导入创建一个新的 chunk 文件。例如,上述代码中的 utils.js 会被打包成一个单独的 utils.[hash].js 文件。
    • 当运行时执行到动态导入语句时,Webpack 会使用 JSONP 来加载这个新的 chunk 文件。这种方式使得应用程序可以按需加载代码,而不是一次性加载所有代码,提高了初始加载速度。

代码分割的策略与优化

  1. 手动代码分割
    • 除了动态导入自动触发代码分割外,还可以使用 splitChunks 进行手动代码分割。例如,将所有的第三方库分割到一个单独的 chunk 中:
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      name:'vendors',
      minSize: 30000,
      minChunks: 1
    }
  }
};
  • chunks: 'all' 表示对所有类型的 chunks 进行分割。name:'vendors' 定义了分割后的 chunk 名称为 vendorsminSize 设置了分割的最小文件大小,只有大于 30000 字节(约 30KB)的 chunk 才会被分割。minChunks 表示模块至少被引用多少次才会被分割,这里设置为 1 表示只要被引用就分割。
  1. 预加载与预渲染
    • 预加载(Preloading)和预渲染(Prerendering)是与代码分割相关的优化技术。预加载可以在浏览器空闲时提前加载可能需要的代码 chunk,而预渲染则是在服务器端提前渲染页面,将渲染好的 HTML 发送到客户端。
    • 在 Webpack 中,可以通过 html - webpack - pluginpreload 选项来实现预加载。例如:
const HtmlWebpackPlugin = require('html - webpack - plugin');

module.exports = {
  //...其他配置
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      inject: true,
      preload: true
    })
  ]
};
  • 这样在生成的 HTML 文件中,会为动态导入的 chunk 添加 <link rel="preload"> 标签,告诉浏览器在空闲时提前加载这些资源。
  • 对于预渲染,可以使用 react - snapshot 等工具在服务器端提前渲染 React 应用,将渲染好的 HTML 发送到客户端,减少客户端的渲染时间,提高用户体验。

性能监控与持续优化

性能监控工具

  1. Webpack 内置性能分析
    • Webpack 提供了一些内置的性能分析工具。通过在 Webpack 配置中添加 performance 选项,可以设置性能提示的阈值。例如:
module.exports = {
  performance: {
    hints: 'warning',
    maxEntrypointSize: 400000,
    maxAssetSize: 300000
  }
};
  • hints 可以设置为 'warning'(默认)、'error'false。当设置为 'warning' 时,如果入口点或单个资源文件大小超过 maxEntrypointSizemaxAssetSize 所设置的阈值,Webpack 会在控制台输出警告信息。设置为 'error' 时则会抛出错误。
  1. Lighthouse
    • Lighthouse 是一款开源的、自动化的网页性能审计工具,集成在 Chrome DevTools 中。它可以对网页的性能、可访问性、最佳实践等方面进行全面评估,并给出详细的报告和优化建议。
    • 要使用 Lighthouse,可以在 Chrome 浏览器中打开需要评估的页面,然后打开 DevTools,切换到 Lighthouse 标签页,点击“Generate report”按钮即可生成报告。报告中会指出页面加载时间、资源大小、渲染性能等方面的问题,并提供具体的优化措施。

持续优化流程

  1. 建立性能基线
    • 在项目开始时,使用性能监控工具(如 Lighthouse)对项目进行一次全面的性能评估,记录下各项性能指标,如首次内容绘制时间(First Contentful Paint)、最大内容绘制时间(Largest Contentful Paint)、总阻塞时间(Total Blocking Time)等,作为性能基线。
    • 随着项目的开发,每次进行重要的代码变更(如添加新功能、优化现有代码等)后,再次使用性能监控工具进行评估,对比新的性能指标与基线。如果性能出现明显下降,需要及时分析原因并进行优化。
  2. 定期性能优化
    • 定期(例如每周或每两周)对项目进行性能检查,不仅仅关注新的代码变更,还需要对整个项目的代码结构、资源使用等方面进行全面审查。
    • 可以通过分析性能报告,找出长期存在的性能瓶颈,如某些体积过大的模块、加载缓慢的资源等,并制定相应的优化计划。例如,对体积过大的模块进行进一步的代码分割,优化资源的加载顺序等。通过持续的性能优化,可以确保项目在整个生命周期内都保持良好的性能表现。