webpack/vue-cli构建速度和打包体积优化

2,802 阅读12分钟

编译优化

jelly.jd.com/article/611…

www.psvmc.cn/article/202…

编译分析插件

webpack-bundle-analyzer

webpack-bundle-analyzer 可以生成代码分析报告,可以直观地分析打包出的文件有哪些,及它们的大小、占比情况、各文件 Gzipped 后的大小、模块包含关系、依赖项等

image-20220825103406352

npm i -D webpackbar webpack-bundle-analyzer

 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 ​
 module.exports = {
   // ...
   plugins: [
     new BundleAnalyzerPlugin(),
   ]
 }
 //package.json
 "scripts": {
    "analyz": "webpack-bundle-analyzer --port 8888 ./build/stats.json",
 }

新版的 vue-cli 也内置了 webpack-bundle-analyzer

 "scripts": {
   "analyz": "vue-cli-service build --report",
 },

配置:

  • analyzerMode:server / static / json / disabled

    默认值:server。 在 server 模式下,分析器将启动 HTTP 服务器以显示 bundle 报告。 在 static 模式下,将生成带有 bundle 报告的单个 HTML 文件。 在 json 模式下,将生成带有捆绑报告的单个 JSON 文件。 在 disable 模式下,您可以使用此插件通过将 generateStatsFile 设置为 true 来生成 Webpack Stats JSON 文件。

  • analyzerHost:默认值:127.0.0.1。 在 server 模式下用于启动 HTTP 服务器的主机。

  • analyzerPort:默认值:8888。在 server 模式下用于启动 HTTP 服务器的端口

  • reportFilename:默认值:report.html。 在 static 模式下生成的捆绑报告文件的路径。 它可以是绝对路径,也可以是相对于 bundle 文件输出目录的路径(在 webpack 配置中是 output.path)。

  • defaultSizes:stat / parsed / gzip

    默认值:parsed。 默认情况下在报告中显示的模块大小。

    stat:这是文件的“输入”大小,在进行任何转换(如缩小)之前。之所以称为“stat size”,是因为它是从 Webpack 的 stats 对象中获取的。

    parsed:这是文件的“输出”大小。 如果你使用的是 Uglify 之类的 Webpack 插件,那么这个值将反映代码的缩小后的大小。

    gzip:这是通过 gzip 压缩运行解析的包/模块的大小。

  • openAnalyzer:默认值:true。 在默认浏览器中自动打开报告。

  • genarateStatsFile:默认值:false。 如果为 true,将在 bundle 输出目录中生成 webpack stats JSON 文件

rollup-plugin-visualizer(vite)

webpackbar

webpackbar 提供了友好的编译进度提示

 const WebpackBar = require('webpackbar');
 ​
 module.exports = {
   // ...
   plugins: [
     new WebpackBar(),
   ]
 }

speed-measure-webpack-plugin

优化 webpack 构建速度,首先需要知道是哪些插件、哪些 loader 耗时长,方便我们针对性的优化。通过 speed-measure-webpack-plugin 插件进行构建速度分析,可以看到各个 loader、plugin 的构建时长,后续可针对耗时 loader、plugin 进行优化。

 npm i -D speed-measure-webpack-plugin

构建速度优化

缓存

缺点
  • cache-loader
  • hard-source-webpack-plugin

以上这些缓存方式都有首次启动时的开销,即它们会让 “冷启动” 时间会更长,但是二次启动能够节省很多时间

Webpack:cache

通过配置 webpack 持久化缓存 cache: filesystem,来缓存生成的 webpack 模块和 chunk,二次进行构建/打包时,可以直接从缓存中拉取,改善构建速度。

  • cache.type

    string: 'memory' | 'filesystem'

    将 cache 类型设置为内存或者文件系统。memory 选项很简单,它告诉 webpack 在内存中存储缓存

     module.exports = {
         cache: {
           type: 'filesystem', // 使用文件缓存
         },
     }
    
babel-loader

babel-loader 的 options 设置中增加 cacheDirectory 属性,属性值为 true。表示:开启 babel 缓存,第二次构建时会读取之前的缓存,构建速度会更快一点。

 {
     test: /.js$/,
     loader: 'babel-loader',
     options: {
         cacheDirectory: true
     }
 }
cache-loader

webpack.docschina.org/loaders/cac…

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

 module.exports = {
     //...
     
     module: {
         //我的项目中,babel-loader耗时比较长,所以我给它配置了`cache-loader`
         rules: [
             {
                 test: /.jsx?$/,
                 use: ['cache-loader','babel-loader']
             }
         ]
     }
 }

如果你跟我一样,只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory。

hard-source-webpack-plugin

HardSourceWebpackPlugin 和 speed-measure-webpack-plugin 不能一起使用。这个插件能正常使用的版本是 webpack5 以下的版本。

npm install --save-dev hard-source-webpack-plugin

为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source

 const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
 module.exports = {
   configureWebpack: config => {
     config.plugin.push(
       // 为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source
       new HardSourceWebpackPlugin({
         root: process.cwd(),
         directories: [],
         environmentHash: {
           root: process.cwd(),
           directories: [],
           files: ['package.json', 'yarn.lock']
         }
       })
       // 配置了files的主要原因是解决配置更新,cache不生效了的问题,配置后有包的变化,plugin会重新构建一部分cache
     )
   }
 }
hash 缓存

防止编译文件名字重复,部署版本的时候,浏览器使用缓存文件。同时,如果编译时文件未改动,不会改变文件名和文件的

hash、chunkhash、contenthash

hash 是一整个项目,一次打包,只有一个 hash 值,是项目级的

chunhash 是从入口 entry 出发,到它的依赖,以及依赖的依赖,依赖的依赖的依赖,等等,一直下去,所打包构成的代码块(模块的集合)叫做一个 chunk,也就是说,入口文件和它的依赖的模块构成的一个代码块,被称为一个 chunk。

contenthash 是哈希只跟内容有关系,内容不变,哈希值不变。与 chunkhash 的区别可以举上面 contenthash 的例子,同时可以说明 contenthash 跟内容有关,但是 chunkhash 会考虑很多因素,比如模块路径、模块名称、模块大小、模块 id 等等。

 output: {
       filename: '[name].[contenthash].js', // contenthash 只有在内容发生改变才会变
       path: path.resolve(__dirname, 'dist'), //输出路径   __dirname 代表当前文件的绝对路径
       clean: true, //在生成文件之前清空 output 目录
     },
 vue-cli    
 configureWebpack -> 
 config.output.filename = `js/[name].[contenthash].js`;
 config.output.chunkFilename = `js/[name].[contenthash].js`;

在提取 css 时我们也可以这么命名文件名

 // css 提取
 plugins: [
   new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:10].css',
   }),
 ]
dll

将我们项目中的依赖使用 dll 插件进行动态链接,这样依赖就不会进行编译,从而极大地提高编译速度

webpack5 开箱即用的持久缓存是比 dll 更优的解决方案

将 dll 和缓存进行对比可以发现:

缓存DLL
把常用的文件存储到内存或硬盘中把公共代码打包为 dll 文件放到硬盘中
再次打包时,直接取读取缓存再次打包时,读取 dll 文件,不重新打包
加载时间减少打包时间减少

多线程

将文件解析任务分解成多个子进程并发执行,发挥多核 CPU 电脑的威力。子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构建速度

happypack

happypack 同样是用来设置多线程,但是在 webpack5 就不要再使用 happypack 了,官方也已经不再维护了,推荐使用 thread-loader。

npm install happypack -D

HappyPack 参数

  • id: String 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件.
  • loaders: Array 用法和 webpack Loader 配置中一样.
  • threads: Number 代表开启几个子进程去处理这一类型的文件,默认是 3 个,类型必须是整数。
  • verbose: Boolean 是否允许 HappyPack 输出日志,默认是 true。
  • threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。
  • verboseWhenProfiling: Boolean 开启 webpack --profile , 仍然希望 HappyPack 产生输出。
  • debug: Boolean 启用 debug 用于故障排查。默认 false
 //提升 Webpack 构建速度
 const HappyPack = require('happypack');
 //安装 OS 模块 这个主要是拿到当前电脑的CPU核数
 const os = require('os');
 //这个是设置共享线程池中的数量 size 控制设置数量 类型 只能是 整数类型
 const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
 ​
 module.exports = {
   module: {
     rules: [
       {
         test: /.js$/,
         //把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
         loader: 'happypack/loader?id=happyBabel',
         //排除node_modules 目录下的文件
         exclude: /node_modules/
       },
       {
             test: /.(css|less)$/,
             use: 'happypack/loader?id=styles'
       },
     ]
   },
 plugins: [
     new HappyPack({
        //用id来标识 happypack处理那里类文件
       id: 'happyBabel',
       //用法和loader 的配置一样
       loaders: [{
         loader: 'babel-loader?cacheDirectory=true',
       }],
       //共享进程池
       threadPool: happyThreadPool,
       //允许 HappyPack 输出日志
       verbose: true,
     }),
     new HappyPack({
         id: 'styles',
         loaders: [ 'style-loader', 'css-loader', 'less-loader' ],
         //共享进程池
         threadPool: happyThreadPool,
     });
   ]
 }

vue

    //把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
    // config.module.rule('js').test(/.js$/)
    //   .include.add('/src/').end()
    //   .exclude.add('/node_modules/').end()
    //   .use().loader('happypack/loader?id=happyBabel').end()
thread-loader
  • Webpack

    npm install --save-dev thread-loader

    const path = require("path");
    module.exports = {
      module: {
        rules: [
          {
            test: /.js$/,
            include: path.resolve('src'),
            use: [
              "thread-loader",
              // 耗时的 loader (例如 babel-loader)
            ],
          },
        ],
      },
    };
    
  • Vue-Cli 已经内置 thread-loaderthread-loader 会在多核 CPU 的机器上为 Babel/TypeScript 转译开启。

    module.exports = {
      parallel: true,
    }
    
    • Type: boolean

    • Default: require('os').cpus().length > 1

      是否为 Babel 或 TypeScript 使用 thread-loader

      该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。

缩小文件检索解析范围

Resolve解析模块路径
  • alias:为避免无用的检索与递归遍历,可以使用 alias 指定引用时候的模块

  • extensions:extensions字段用来在导入模块时,自动带入后缀尝试去匹配对应的文件。由于 webpack 的解析顺序是从左到右,因此要将使用频率高的文件类型放在左侧,如下我将 tsx 放在最左侧

    module.exports = {
        resolve: {
            extensions: ['.tsx', '.js'], // 因为我的项目只有这两种类型的文件,如果有其他类型,需要添加进去。
        }
    }
    
noParse

一些第三方模块没有使用AMD/CommonJs规范,可以使用noParse来标记这个模块,这样Webpack在导入模块时,就不进行解析和转换,提升Webpack的构建速度

//可以接受一个正则表达式或者一个函数
{
    module: {
        //noParse: /jquery|lodash|chartjs/,
        noParse: function(content){
            return /jquery|lodash|chartjs/.test(content)
        }
    }
}
include/exclude

include表示哪些目录中的文件需要进行babel-loader,exclude表示哪些目录中的文件不要进行babel-loader。这是因为在引入第三方模块的时候,很多模块已经是打包后的,不需要再被处理,比如vue、jQuery等;如果不设置include/exclude就会被loader处理,增加打包时间。

 {
     rules: [{
         test: /.js$/,
         use: {
             loader: 'babel-loader'
         },
         // exclude: /node_modules/,
         include: [path.resolve(__dirname, 'src')]
     }]
 }

预加载

Preload和Prefetch是两种优化前端性能的技术,它们可以让浏览器在某些条件下提前加载一些资源,从而加快应用程序的加载速度。

  • Preload告诉浏览器立即加载资源。Preload可以使用rel="preload"属性来实现,比如:

     <link rel="preload" href="path/to/resource" as="type"/>
    

    href表示需要预加载的资源路径,as属性指定预加载资源的类型

  • Prefetch告诉浏览器在空闲时才开始加载资源。Prefetch的实现方式是通过添加rel="prefetch"属性来实现

     <link rel="prefetch" href="path/to/resource"/>
    

需要注意的是,Preload和Prefetch不是浏览器强制加载资源,而是给浏览器提供了一些提示,让它在空闲时间主动加载一些资源,从而提升应用程序的性能体验。这两项技术它们的兼容性不是很好

prefetch

下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

 //...
 import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到index.html,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。

代码分离

www.cnblogs.com/Mr-Hou88888…

blog.csdn.net/qq_41887214…

代码分离 code splitting 把代码分离到不同的 bundle( Chunk 是打包产物的基本组织单位) 中用于获取更小的 bundle,然后可以按需加载或并行加载这些文件,控制资源加载优先级,提供代码的加载性能

  • 「资源冗余」:客户端必须等待整个应用的代码包都加载完毕才能启动运行,但可能用户当下访问的内容只需要使用其中一部分代码
  • 「缓存失效」:将所有资源达成一个包后,所有改动 —— 即使只是修改了一个字符,客户端都需要重新下载整个代码包,缓存命中率极低

常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk
  • 动态导入:通过模块的 内联函数 import 调用来分离代码。

optimization

optimization 用于自定义 webpack 的内置优化配置,一般用于生产模式提升性能,常用配置项如下:

  • minimize:是否压缩代码,默认为 true。

  • minimizer:配置压缩工具,常用的压缩工具有 UglifyJS、TerserJS 和 CSSMinimizerPlugin 等

  • splitChunks:代码分割,将公共代码提取出来,避免重复打包。

    • chunks 指的是分离包的作用范围。"initial"(同步包) | "all"(推荐,同步或异步包) | "async" (默认就是async,异步包)

    • cacheGroups:通过 cacheGroups,我们可以定义自定义 chunk 组,通过 test 条件对模块进行过滤,符合条件的模块分配到相同的组。

      • test: /[/]node_modules[/]/
      • name: 'vendors', // 打包出来的文件名
       lodash: { // 针对lodash的特定规则
         test: /[\/]node_modules[\/]lodash[\/]/,
         name: 'lodash', // 自定义文件名
         chunks: 'all',
       },
      
  • runtimeChunk:除业务代码外,Webpack 编译产物中还需要包含一些用于支持 webpack 模块化、异步加载等特性的支撑性代码,这类代码在 webpack 中被统称为 runtime。虽然每段运行时代码可能都很小,但随着特性的增加,最终结果会越来越大,特别对于多 entry 应用,在每个入口都重复打包一份相似的运行时代码显得有点浪费,为此 webpack 5 专门提供了 entry.runtime 配置项用于声明如何打包运行时代码。将运行时代码单独打包成一个文件,避免重复打包。

    • 模块解析‌:运行时代码负责解析模块的依赖关系,确定哪些模块需要加载。
    • 模块加载‌:负责加载模块,包括初始加载和按需加载的模块。
    • 缓存管理‌:帮助管理浏览器缓存,确保模块在更新后能够正确加载。
    • 热模块替换‌:在开发环境中,支持热模块替换(HMR),允许在不刷新页面的情况下更新模块‌1。

    在多 entry 场景中,只要为每个 entry 都设定相同的 runtime 值,webpack 运行时代码最终就会集中写入到同一个 chunk,例如对于如下配置:

     module.exports = {
       entry: {
         index: { import: "./src/index", runtime: "solid-runtime" },
         home: { import: "./src/home", runtime: "solid-runtime" },
       }
     };
    
  • usedExports:是否只导出被使用的代码。

  • sideEffects:是否开启副作用标记,用于 tree shaking。

  • concatenateModules:是否开启模块合并。

  • emitOnErrors:是否在编译出错时生成文件

 module.exports = {
   optimization: {
     runtimeChunk: 'single',
     minimizer: [
       new CssMinimizerPlugin(),
     ],
     splitChunks: {
       chunks: 'all',
       minSize: 30000,
       maxSize: 0,
       minChunks: 1,
       maxAsyncRequests: 5,
       maxInitialRequests: 3,
       automaticNameDelimiter: '~',
       name: true,
       cacheGroups:{
         vendors:{ //node_modules里的代码
           test: /[\/]node_modules[\/]/,
           chunks: "all",
           name: 'vendors', //chunks name
           priority: 10, //优先级
           enforce: true 
         }
       }
     },
   },
 }  

多入口起点

入口起点(entry points)

src/index.js

 console.log('Hello world!');

src/another-module.js

 import _ from 'lodash'
 ​
 console.log(_.join(['another', 'module', 'chunk'], ' '));

这个模块依赖了 lodash ,需要安装一下:

 npm install lodash

webpack.config.js

 module.exports = {
   mode: 'development',
   entry: { // 配置多入口文件
     index: './src/index.js',
     another: './src/another_module.js'
   },
    output: {
       filename: 'bundle.js',
       path: path.resolve(__dirname, './dist'),
     },
 }
 ​

执行 webpack 命令,可以看到报错了Error: Entry index depends on common_chunk, but this entry was not found 在这里插入图片描述

这个错误表明发生了冲突,多个入口文件打包后出现了相同的 filename,所以我们要对多个入口文件设置多个出口不同文件名文件

 module.exports = {
  mode: 'development',
   entry: {
     index: './src/index.js',
     another: './src/another_module.js'
   },
   output: {
     filename: '[name].bundle.js', // 对应多个出口文件名,name对应的是entry的属性名 对应index和another
     path: path.resolve(__dirname, './dist'),
   },
 }

执行 webpack 命令,可以看到不报错了,并且 dist 输出了两个 js 文件 在这里插入图片描述 在这里插入图片描述

文件 another.bundle.js 来源于 entry.another,即 src/another.js,文件大小为 554kb,因为被 lodash 被打包进去了

文件 index.bundle.js 来源于 entry.index,即 src/index.js,文件大小为 1.21kb

防止重复

如果我们的其他入口也需要使用 lodash 呢?

 src/index.js
 import _ from 'lodash'
 ​
 console.log(_.join(['index', 'module', 'chunk'], ' '));

在这里插入图片描述

lodash 在两个引用文件中都被打包了,我们期望 lodash 应该是公用的

配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块

 module.exports = {
  mode: 'development',
   entry: {
     index: {
       import: './src/index.js', // 启动时需加载的模块
       dependOn: 'common_chunk', // 当前入口所依赖的入口
     },
     another: {
       import: './src/another_module.js',
       dependOn: 'common_chunk',
     },
     common_chunk: 'lodash' // 当上面两个模块有lodash这个模块时,就提取出来并命名为common chunk
   },
   output: {
     filename: '[name].bundle.js', // 对应多个出口文件名
     path: path.resolve(__dirname, './dist'),
   },
 }
 ​

执行 webpack 命令,可以看到打包结果 在这里插入图片描述

已经提取出来 common_chunk.bundle.js,即为提取打包了 lodash 公用模块。index.bundle.js another.bundle.js 体积也变小

SplitChunksPlugin

但是不能每有一个依赖就需要配置common_chunk

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的 chunk 中,或者提取到一个新生成的 chunk。

webpack.config.js

 module.exports = {
   entry: { // 多入口
     index: './src/index.js',
     another: './src/another_module.js',
   },
   output: {
     filename: '[name].bundle.js', // 对应多个出口文件名
     path: path.resolve(__dirname, './dist'),
   },
   optimization: {
     splitChunks: { // 代码分割
       chunks: 'all' 
     }
   },
 }
 ​

使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.jsanother.bundle.js 中已经移除了重复的lodash依赖模块。 在这里插入图片描述

动态导入/按需加载

概念

webpack的按需加载是一种优化技术,允许应用在需要某个资源或模块时才进行加载,而不是在初始加载时加载所有资源。这样可以显著提高应用的启动速度,减少初始加载的资源体积,节省带宽和流量。

import() 动态的加载模块。调用 import 的之处,被视为分割点,即被请求的模块和它引用的所有子模块,会分割到一个单独的 chunk 中。

注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。

 function(string path):Promise

chunk 模块命名

  • 设置webpackChunkName: "name":这是webpack动态导入模块命名的方式,打包会生成对应模块名的文件

    image-20250301212448985

      //webpack.config.js
      output: {
         filename: "js/[name].bundle.js",
         chunkFilename: "js/[name].chunk-test.js",//打包动态导入代码
         path: path.join(__dirname, 'dist'),
         clean: true,
       },
    
     const useDynamicImport = await import(
     /* webpackChunkName: "dynamicImport" */
     './dynamicImport.js')
    
  • 如果不设置,加载的脚本将被按数字次序命名

场景
  • 路由懒加载

  • 封装一个 component.js,返回一个 component 对象;

    image-20220331133028694

  • 按需加载

     //index.js
     setTimeout(async function () {
       //文件会等5秒后加载
       // webpackChunkName: "name":这是webpack动态导入模块命名的方式
       const useDynamicImport = await import(
         /* webpackChunkName: "dynamicImport" */
         './dynamicImport.js')
       console.log(useDynamicImport.default(1, 2))
     }, 2000)
     ​
     ​
     //dynamicImport.js
     export default function add(a,b){
       return a+b;
     }
    
babel-plugin-dynamic-import-node

babel-plugin-dynamic-import-node 是一个 Babel 插件,它用于将动态 import() 语句转换为 CommonJS 的 require() 调用。如果你在使用 Node.js 并希望利用动态导入的特性(比如在打包工具中或在某些构建场景下),你可以通过使用此插件

  1. Node.js 环境支持

    • 在 Node.js 环境中,动态 import() 语法可能不被原生支持(特别是在较旧版本的 Node.js 中)。这个插件可以将动态 import() 转换为 Node.js 的 require() 调用,从而在 Node.js 环境中正常工作。
  2. 开发环境优化

    • 在开发环境中,特别是当你需要快速启动服务器或进行热重载时,使用 require() 可以减少启动时间,因为 require() 是同步加载模块,而动态 import() 是异步加载。
  3. 代码兼容性

    • 如果你的项目需要在不同环境下运行,并且某些环境不支持动态 import(),这个插件可以帮助你确保代码在这些环境中也能正常运行。
  • 安装:npm install --save-dev babel-plugin-dynamic-import-node

  • 配置

    • vue-cli3

      修改 babel.config.js 文件

      module.exports = {
        presets: ["@vue/cli-plugin-babel/preset"],
        env: {
          development: {
            plugins: ["dynamic-import-node"]
          }
        }
      };
      
    • vue.cli2

      .babelrc 文件

      "env": {
      "test": {
      "plugins": []
      },
      "development":{
      "presets": ["env", "stage-2"],
      "plugins": ["dynamic-import-node"]
      }
      }
      

打包优化

按需打包

在主文件或者组件文件中引入其他模块中的代码,但实际上我们只用其中的一部分,剩下的代码则不需要引入

Tree Shaking

cloud.tencent.com/developer/a…

tree shaking 是一个术语,用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import export

Tree Shaking 只支持 ESM 的引入方式,不支持 Common JS 的引入方式。

  • ESM: export + import
  • Common JS: module.exports + require

开发环境下的配置

 // webpack.config.js
 module.exports = {
   // ...
   mode: 'development',
   optimization: {
     usedExports: true,
   }
 };

生产环境下的配置

 // webpack.config.js
 module.exports = {
   // ...
   mode: 'production',
 };

sideEffects

  • sideEffects: 默认为 true, 告诉 Webpack ,所有文件都有副作用,他们不能被 Tree Shaking。
  • sideEffects: 为 false 时,告诉 Webpack ,没有文件是有副作用的,他们都可以 Tree Shaking
  • sideEffects: 为一个数组时,告诉 Webpack ,数组中那些文件不要进行 Tree Shaking,其他的可以 Tree Shaking

如果你的代码确实有一些副作用,可以改为提供一个数组:

 {
   "name": "your-project",
   "sideEffects": ["./src/some-side-effectful-file.js"]
 }

所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

 {
   "name": "your-project",
   "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
 }
 //或者
  rules: [
      {
          test: /.css$/i,
          use: ["style-loader", "css-loader"],
          sideEffects: true
      }
  ]
IgnorePlugin
  1. 这是 webpack 内置插件
  2. 这个插件的作用是:忽略第三方包指定目录,让这些指定目录不要被打包进去
 //虽然我设置了语言为中文,但是在打包的时候,是会将所有语言都打包进去的。这样就导致包很大,打包速度又慢
 plugins:[
   new Webpack.IgnorePlugin(/./locale/,/moment/),//moment这个库中,如果引用了./locale/目录的内容,就忽略掉,不会打包进去
 ]
lodash
  • 使用时直接访问对应的方法文件。但是有缺点,lodash.com/per-method-…

     //按需打包
     import trim from 'lodash/trim'
     //  or
     // const trim = require('lodash/trim.js')
     ​
     console.log(trim(' 123123 '))
    
  • 通过lodash-es,Lodash 提供 lodash-es 版本以支持 Tree Shaking。

  • babel-plugin-lodash, & lodash-webpack-plugin

     import _ from 'lodash'
     import { add } from 'lodash/fp'
      
     const addOne = add(1)
     _.map([1, 2, 3], addOne)
     ​
     //roughly to
     import _add from 'lodash/fp/add'
     import _map from 'lodash/map'
      
     const addOne = _add(1)
     _map([1, 2, 3], addOne)
    

babel

console 移除

babel-plugin-transform-remove-console 插件,配置在 babel.config.js 中,vue-cli5 实测可行,vue-cli3,4 也可行。(尝试后,谷歌浏览器控制台仅 websocket 的打印输出未清除,其他打印输出都是清除干净了的)

下载依赖 npm install babel-plugin-transform-remove-console -D

babel.config.js 中

 const proPlugins = [];
 // 判断环境
 if (process.env.NODE_ENV === 'production') {
   proPlugins.push('transform-remove-console');
 }
 module.exports = {
   plugins: [...proPlugins],
 };
辅助代码

Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!默认情况下会被添加到每一个需要它的文件中。可以将这些辅助代码作为一个独立模块,来避免重复引入

@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtiome 并且使所有辅助代码从这里引用

npm i @babel/plugin-transform-runtime -D

img

js 压缩

使用 TerserPlugin 来压缩和丑化 JavaScript 文件,移除无用代码、空白符等,减少体积

webpack5 自带最新的 terser-webpack-plugin,无需手动安装。

terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量: os.cpus().length - 1 ,使用多进程并发运行压缩以提高构建速度。

 const TerserPlugin = require('terser-webpack-plugin');
 module.exports = {
   optimization: {
     minimizer: [
       new TerserPlugin({
         test: /.js(?.*)?$/i,
         parallel: true,
         extractComments: true,
         sourceMap: config.build.productionSourceMap,
         terserOptions: {
           output: {
             // 是否输出可读性较强的代码,即会保留空格和制表符,默认为输出,为了达到更好的压缩效果,可以设置为false
             beautify: false,
             // 是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false
             comments: false
           },
           compress: {
             // 是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出,可以设置为false关闭这些作用不大的警告
             warnings: false,
             // 是否删除代码中所有的console语句,默认为不删除,开启后,会删除所有的console语句
             drop_console: true,
             drop_debugger: true,
             // 是否内嵌虽然已经定义了,但是只用到一次的变量,比如将 var x = 1; y = x, 转换成 y = 5, 默认为不转换,为了达到更好的压缩效果,可以设       置为false
             collapse_vars: true,
             // 是否提取出现了多次但是没有定义成变量去引用的静态值,比如将 x = 'xxx'; y = 'xxx'  转换成var a = 'xxxx'; x = a; y = a; 默认为       不转换,为了达到更好的压缩效果,可以设置为false
             reduce_vars: true,
             pure_funcs: ['console.log'] // 移除console
           }
         }
       }),
     ]
   }
 }
vue-cli
 config.optimization.minimize(true)// 开启压缩js代码
 config.optimization.splitChunks({ // 开启代码分割
     chunks: 'all'
 })
 ​
 const TerserPlugin = require("terser-webpack-plugin")
 chainWebpack: config => {
     
     config.optimization.minimize(true);// 开启压缩js代码
     config.optimization.minimize(new TerserPlugin({
                 terserOptions:{
                   compress:{
                     warnings: false,
                     drop_console: true,
                     drop_debugger: true,
                     pure_funcs: ["console.log"]
                   }
                 }
               }));
     config.optimization.splitChunks({// 开启代码分割
         chunks: 'all',
     });
     
 }

css

www.jianshu.com/p/e92b5bccf…

blog.csdn.net/qq_34569497…

css 抽离

mini-css-extract-plugin 插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载

 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 ​
 module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // "style-loader",
          MiniCssExtractPlugin.loader,
          "css-loader",
          // 如果想要启用 CSS 模块化,可以为 css-loader 添加 modules 参数即可
        ],
      },
    ],
   plugins: [
     new MiniCssExtractPlugin({
       filename: "css/[name].[hash].css", // 定义抽离的入口文件的文件名
       chunkFilename: "css/[name].[hash].css", // 定义非入口块文件的名称,如动态导入的文件
     }),
   ],     
 },
css 压缩

这将仅在 mode: production 生产环境 开启 CSS 优化

如果还想在 开发环境 下启用 CSS 优化,optimization.minimize 设置为 true

 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
 ​
 module.exports = {
   module: {
     rules: [
       {
         test: /.(css|less)$/,
         use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
       },
     ],
   },
 + optimization: {
 +   minimizer: [
 +     new CssMinimizerPlugin(),
 +   ],
 + },
 };

img 压缩

image-minimizer-webpack-plugin: 用来压缩图片的插件

npm i image-minimizer-webpack-plugin imagemin -D 还有剩下包需要下载,有两种模式:

无损压缩 npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D 有损压缩 npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

gzip 压缩

前端将文件打包成 .gz 文件,然后通过 nginx 的配置,让浏览器直接解析 .gz 文件,可以大大提升文件加载的速度,浏览器可以直接解析 .gz 文件并解压。

 启用gzip压缩(需要配置nginx,可以看出压缩后的文件大小明显变化)
 highlighter- PHP
 ​
 const CompressionWebpackPlugin = require('compression-webpack-plugin')
 chainWebpack(config) {
   // 生产模式下启用gzip压缩 需要配置nginx支持gzip
     if (process.env.NODE_ENV === 'production') {
       config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin, [
         {
           filename: '[path][base].gz',
           algorithm: 'gzip',
           test: new RegExp('\.(js|css)$'),
           // 只处理大于xx字节 的文件,默认:0
           threshold: 10240,
           // 示例:一个1024b大小的文件,压缩后大小为768b,minRatio : 0.75
           minRatio: 0.8, // 默认: 0.8
           // 是否删除源文件,默认: false
           deleteOriginalAssets: false
         }
       ])
     }
 }

配置 CDN

线上使用 cdn , 如何库有问题,项目就会有问题,除非公司有自己的 cdn 库。它的配置也很简单,在 externals 中配置

 module.exports = {
   configureWebpack: config => {
     if (process.env.NODE_ENV === 'production') {
       // 配置 cdn,这里将 vue,vue-router 和 axios 三个包配置成 cdn 引入
       // 其中 Vue,VueRouter 等名称是该库暴露在全局中的变量名
       config.externals = {
         vue: 'Vue',
         'vue-router': 'VueRouter',
         axios: 'axios'
       }
     }
   }
 }

然后在 public/index.html 模板文件中引入 cdn 地址:

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
     <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
     <title></title>
     <!-- 引入 cdn 地址 -->
     <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.5.10/vue.min.js"></script>
     <script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.1/vue-router.min.js"></script>
     <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.18.0/axios.min.js"></script>
   </head>
   <body>
     <div id="app"></div>
   </body>
 </html>

我这里使用的是 bootcdn 的地址,需要注意版本问题。

也可以 借助 HtmlWebpackPlugin 插件 来方便插入 cdn 的引入。

使用 cdn 引入的方式虽然能极大改善网页加载速度,但我还是不会用这个功能,项目还不需要非得这样的优化,也怕 cdn 不稳定。

借助 HtmlWebpackPlugin 插件来方便插入 cdn 的引入

 //生产环境标记
 const IS_PRODUCTION = process.env.NODE_ENV === "production";
 const path = require("path");
 // 生产配置
 const cdn_production = {
   js: ["/librarys/vue@2.6.11/vue.min.js"]
 };
 // 开发配置
 const cdn_development = {
   js: ["/librarys/vue@2.6.11/vue.js"]
 };
 ​
 module.exports = {
   configureWebpack: {
     externals: {
       vue: "Vue",
     },
   },
   chainWebpack: config => {
     config.plugin("html").tap(args => {
       args[0].cdn = IS_PRODUCTION ? cdn_production : cdn_development;
       return args;
     });
   }
 };

index.html 中添加

 <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
   <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
 <% } %>
vite

不需要配置 externals,且开发环境还是要下载依赖

  • 依赖安装 npm i vite-plugin-cdn-import -D

  • 使用方法及引入

          importToCDN({
             // prodUrl:可选,默认指向 https://cdn.jsdelivr.net/npm/{name}@{version}/{path}
             modules: [
               {
                 name: 'jquery',
                 var: 'jQuery',
                 version: '3.6.4',
                 path: 'dist/jquery.min.js'
               }
               // {
               //   name: 'element-plus',
               //   // ElementPlus 为什么不是同下面第二种配置的elementPlus是因为这个配置同CDN资源一致,而下面的配置同需同main.ts的引入名称一致
               //   var: 'ElementPlus', // 外部化的依赖提供一个全局变量 同rollupOptions配置中的globals的值
               //   // https://unpkg.com/element-plus@2.2.32/dist/index.full.js 或者 dist/index.full.js
               //   path: 'dist/index.full.js',
               //   // 可选
               //   css: 'dist/index.css'
               // },
              
             ]
           })
    

使用第三方库的精简版本

避免在生产环境下才会用到的工具

某些 utility, plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin 来 minify(压缩) 和 mangle(混淆破坏) 代码是没有意义的。通常在开发环境下,应该排除以下这些工具:

  • TerserPlugin
  • [fullhash]/[chunkhash]/[contenthash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin