webpack性能优化和构建优化

进行优化的第一步需要知道我们的构建到底慢在那里。通过 speed-measure-webpack-plugin 测量构建期间各个阶段花费的时间,bundle-analyzer-plugin可以查看构建包的bundle的大小。我们可以根据相关信息去优化。

构建性能

缩小查找范围

exclude/include

确定 loader的规则范围,避免不必要的转译

resolve

  1. 对庞大的第三方模块设置 resolve.alias , 使webpack直接使用库的min文件,减少路径解析计算
js 复制代码
resolve: {
  alias:{
    'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
  }
}
  1. 限定文件扩展名(extensions)
js 复制代码
resolve: {
  extensions: ['.tsx', '.js', '.json']  // 按使用频率排序
}
  • 避免无后缀文件的多重查找(如 import './data' 依次尝试 .tsx → .js → .json
  • 高频扩展名前置可减少 5%-10% 的解析时间

module.noParse 减少模块解析

适用场景:处理未采用模块化标准且无依赖的第三方库(如 jQuery、Lodash)

原理:跳过对指定模块的 AST 解析和依赖分析

js 复制代码
// webpack.config.js
module.exports = {
  module: {
    noParse: 
      // 正则表达式匹配
      /jquery|lodash/,
      
      // 或函数式匹配(Webpack 3+)
      (content) => /jquery|lodash/.test(content)
  }
};

效果:

  • 减少 10%-20% 的构建时间(对大型库更明显)
  • 需确保目标模块不含 require/import 等模块化语法,否则会导致运行时错

多线程操作

thread-loader(多进程Loader)

多线程运行loader,也就是放置在这个loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行

注意:线程启动开销较大,小型项目优化效果不明显,如果大型项目(文件超过100个)再启动

多进程压缩JS文件

目前使用TerserPlugin可开启parallel属性,也可以使用ParallelUglifyPlugin插件。

缓存

缓存loader的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变

于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果

cache-loader可以实现这样的功能

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        use: ['cache-loader', ...loaders]
      },
    ],
  },
};

有趣的是,cache-loader放到最前面,却能够决定后续的loader是否运行

持久化缓存

Webpack 5 引入了持久化缓存功能,可以显著提升构建性能。这个功能会将首次构建的结果保存到本地文件系统,在下次构建时,它会比对文件的哈希值或时间戳,如果文件没有变化,就直接使用缓存的结果,从而提升构建性能。

要开启 Webpack 的持久化缓存,你可以在 Webpack 配置 cache设置:

js 复制代码
module.exports = {
  // ...其他配置...
  cache: {
    type: 'filesystem', // 将缓存存储在文件系统中,区别于默认的内存缓存
    cacheDirectory: path.resolve(__dirname, '.webpack_cache'),// 可选:设置缓存文件存放的路径,默认为 node_modules/.cache/webpack
    buildDependencies: {
      config: [__filename], // 指定额外依赖;例如:当 webpack 配置文件本身发生变化时缓存将自动失效
    },
  },
};

DllPlugin(动态链接库)

减少基础模块编译次数,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取。为什么会提升构建速度呢?原因在于dll中大多包含的是常用的第三方模块,如react、react-dom不会经常变动,只要编译打包过一次后续直接引用,避免反复编译。

  1. 打包公共模块,暴露变量名,生成资源清单
js 复制代码
const webpack = require("webpack")
const path = require('path')
const ROOT_PATH = path.resolve(__dirname, '../')
const BUILD_PATH = path.resolve(ROOT_PATH, './dll')

module.exports = {
  entry: {
    //将 react、lodash等模块作为入口编译成动态链接库,可以理解为单独打包
    lib: ['react', 'react-dom','lodash']
  },
  output: {
    //指定路径
    path: BUILD_PATH,
    //指定文件名
    //这个名称需要与 DllPlugin 插件中的 name 属性值对应起来
    library: '_lib_[name]',
    filename: 'dll.lib.js'
  },
  plugins: [
    new webpack.DllPlugin({
      //和output.library中一致,值就是输出的manifest.json中的 name值
      name: '_lib_[name]',
     	path: path.resolve(BUILD_PATH, 'manifest.json')
    })
  ]
}
  1. 利用DLLReferencePlugin 根据资源清单文件去获取打包好的公共模块、并且动态将./static/dll的js文件在html中引入即可
js 复制代码
const webpack = require("webpack")
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
  entry: {
    //...
  },
  output: {
    //...
    clean: false, // 清理文件,热更新时也会清理dll引用,导致热更新再刷新浏览器异常...
  },
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: './dll/manifest.json'
    }),
    new AddAssetHtmlPlugin(
        {
          filepath: './dll/dll.lib.js',
          outputPath: './dll', //相对于 output.path 的相对路径。
          publicPath: 'dll',
        }
    ),
  ]
}

完整配置参考:

  1. github.com/wqhui/react...
  2. github.com/wqhui/react...

优化开发体验

watch

Webpack可以使用两种方式开启监听:

  1. 启动webpack时加上--watch参数;
  2. 在配置文件中设置watch:true

此外还有如下配置参数。合理设置watchOptions可以优化监听体验。

js 复制代码
module.exports = {
    watch: true,
    watchOptions: {
        ignored: /node_modules/,
        aggregateTimeout: 300,  //文件变动后多久发起构建,越大越好
        poll: 1000,  //每秒询问次数,越小越好
    }
}

cross-env

运行跨平台设置和使用环境变量的脚本。

当我们使用在scripts 中使用 NODE_ENV = production 来设置环境变量的时候,mac上是可以正常运行,但是windows通常会报错。因此 cross-env 出现了, cross-env 提供一个可兼容多平台的设置环境变量的scripts

json 复制代码
//package.json
"scripts": {
  "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack/webpack.dev.js --open"
},
  
//webpack.config.js
console.log(process.env.NODE_ENV)//development

当你在不同设备运行 npm run dev 时,其都可以正常的设置NODE_ENV

注意:该参数配置的环境变量,仅是 node 环境下的变量 ,也就是我们webpack配置文件中process.env.NODE_ENV的值。而各个模块中(我们的JS源码)中process.env.NODE_ENV的值是通过webpack配置参数mode 或者是 DefinePlugin定义的

BundleAnalyzerPlugin

打包分析,可以查看打包后的文件信息,方便优化。

js 复制代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const config = {
	    plugins:[ 
        // 配置打包分析 
        new BundleAnalyzerPlugin({
          // analyzerMode: 'disabled',  // 不启动展示打包报告的http服务器
          // generateStatsFile: true, // 是否生成stats.json文件
        })
    ]
}

完整配置参考:github.com/wqhui/react...

SpeedMeasurePlugin

可以使用speed-measure-webpack-plugin来测量打包的耗费时间

js 复制代码
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [new MyPlugin(), new MyOtherPlugin()],
});

完整配置参考:github.com/wqhui/react...

生产包优化(压缩、拆包、删除无用代码)

optimization.minimizer代码压缩

该配置不仅可以配置压缩JS代码,webpack内置了TerserPlugin;同时可以使用CssMinimizerPlugin压缩CSS代码。

js 复制代码
{
    optimization: {
        minimizer: [
            //压缩css
            new CssMinimizerPlugin(
                {
                    exclude: PATHS.nodeModulesPath
                }
            ),
            //压缩js 也可以使用...扩展现有的
            new TerserPlugin({}),
        ]
    }    
}

optimization.splitChunks 静态分析代码分包

在构建时静态地分析和拆分模块,用于优化代码的静态分割,分包是从已有的chunk中分离出新的chunk

详细可查看官方文档webpack SplitChunksPlugin实用指南

js 复制代码
{
    optimization: {
        splitChunks: {
            maxInitialRequests: 5,
            
            cacheGroups: {
                //提取src代码
                common: {
                    name: 'chunk-common',
                    test: /[\/]src[\/]/,
                    //可选值有 async、 initial 和 all。
                    //默认值是 async,也就是默认只选取异步加载的chunk进行代码拆分。
                    //initial 也就是默认同步加载的代码
                    //all 上述两种情况都涵盖
                    chunks: 'all',
                    // 拆分前必须共享模块的最小 chunks 数,也就当前的文件被1个以上的文件引用时才拆分
                    minChunks: 1,
                    //生成 chunk 的最小体积(以 bytes 为单位)
                    minSize: 0, 
                    //一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority(优先级)的缓存组
                    priority: 1,
                },
                //分割node包代码
                vendors: {
                    name: 'chunk-vendors',
                    test: /[\/]node_modules[\/]/,
                    chunks: 'all',
                    priority: 2,
                }
            }
        }
    }    
}

相关参数说明:

  1. cacheGroups

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

  1. chunks

该配置项用于配置需要应用分包策略的chunk

我们知道,分包是从已有的chunk中分离出新的chunk,那么哪些chunk需要分离呢

chunks有三个取值,分别是:

  • all: 对于所有的chunk都要应用分包策略
  • async:【默认】仅针对异步chunk应用分包策略
  • initial:仅针对普通chunk应用分包策略

所以,你只需要配置chunksall即可

  1. maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和tree shaking

  1. automaticNameDelimiter:新chunk名称的分隔符,默认值~

  2. minChunks:一个模块被多少个chunk使用时,才会进行分包,默认值1

  3. minSize:当分包达到多少字节后才允许被真正的拆分,默认值30000

import 动态引入代码分包

当涉及到运行时按需加载,可以使用 import()语法 来实现动态导入

js 复制代码
import { lazy } from 'react'
export const Icon = lazy(() => import('./Icon')) 

这个时候我们使用webpack打包,webpack会自动帮我们将代码打包到1.js的文件中,这样就可以减少初始包中代码打大小。

项目中组件很多异步加载的组件,这部分文件可以单独打包(默认会自动打包),提高加载的速度,而这些打包出来的文件名默认为[id].js,不好辨认.如果我们希望对导出的模块的文件进行命名,就需要用到魔法注释

魔法注释 就是在import语句中添加注释,可以对chunk命名或者进行其他操作,具体可查看 Magic-Comments ,我们下面讲讲常用的通过魔法注释的方式:/*webpackChunkName:"[request]"*/,可以在代码里指定不同的文件名。

  • React写法
js 复制代码
import { lazy } from 'react'
export const Icon = lazy(() => import(/* webpackChunkName: "Com-1" */ './Icon')) 
export const Button = lazy(() => import(/* webpackChunkName: "Com-1" */ './Button')) 

Icon 和 Button 组件都会打包到Com-1.js中。

  • 原生JS写法
js 复制代码
// 自定义打包chunk名
import(/*webpackChunkName:'lazy'*/ './src/Lazy.js').then(({doLazySometing})=>{
    doLazySometing()
})

Tree shaking 删除未引用代码

JS

Tree-Shaking 主要用来删除ES6模块中没有引用的模块,webpack 默认在生产模式下才会启用。不过,我们可以通过调试的方式看看他是这么工作的:

  1. 可以使用optimization.usedExportsoptimization.minimize来删除export 导出了的,但是没有地方 import的代码:
js 复制代码
module.exports = {
  mode: 'none',//非生产、或者开发模式
  optimization:{
    //只导出被引用的变量
    usedExports: true,
    //尽可能合并模块到一个函数中
    concatenateModules: true,
    // usedExports后,TerserPlugin 或其它在 optimization.minimizer定义的插件压缩 bundle,删除无用代码。
    minimize: true,
  }
}

需要注意的是,webpackTree shaking的实现前提是模块采用ES Module的方式实现,如果我们使用了babel-loader去转换代码,且babel-loader相关插件将代码转化成了commonJS模块的话,该功能会失效。

  1. 如果我们有一个模块完全没有被引用,想删除整个模块文件,就需要webpack设置optimization.usedExportspackage.json中设置 "sideEffects":false
js 复制代码
module.exports = {
  mode: 'none',//非生产、或者开发模式
  optimization:{
    //只导出被引用的变量
    usedExports: true,
    // usedExports后,TerserPlugin 或其它在 optimization.minimizer定义的插件压缩 bundle,删除无用代码。
    minimize: true,
    sideEffects: true
  }
}
json 复制代码
// 情况1:全包无副作用(适用于纯工具库)
{
  "sideEffects": false
}

// 情况2:特定文件有副作用(如全局样式、初始化脚本)
{
  "sideEffects": ["./src/None.js"]
}

比如我们有个None.js的模块,只是在index.js引用了

js 复制代码
//None.js
export function None(){
    console.log('None')
}

console.log('None component sideEffects')

//index.js
import './src/None.js'

这个时候在生产时,None.js在打包时会被整个删除。

CSS (purgecss-webpack-plugin)

删除无用的css代码可以使用插件

相关推荐
喝拿铁写前端6 分钟前
从圣经Babel到现代编译器:没开玩笑,普通程序员也能写出自己的编译器!
前端·架构·前端框架
HED13 分钟前
VUE项目发版后用户访问的仍然是旧页面?原因和解决方案都在这啦!
前端·vue.js
拉不动的猪34 分钟前
前端自做埋点,我们应该要注意的几个问题
前端·javascript·面试
王景程44 分钟前
如何测试短信接口
java·服务器·前端
安冬的码畜日常1 小时前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程
小杨升级打怪中1 小时前
前端面经-JS篇(三)--事件、性能优化、防抖与节流
前端·javascript·xss
清风细雨_林木木1 小时前
Vue开发网站会有“#”原因是前端路由使用了 Hash 模式
前端·vue.js·哈希算法
鸿蒙布道师2 小时前
OpenAI为何觊觎Chrome?AI时代浏览器争夺战背后的深层逻辑
前端·人工智能·chrome·深度学习·opencv·自然语言处理·chatgpt
袈裟和尚2 小时前
如何在安卓平板上下载安装Google Chrome【轻松安装】
前端·chrome·电脑
曹牧2 小时前
HTML字符实体和转义字符串
前端·html