Webpack

309 阅读9分钟

本文将此前混在Vue文中的Webpack工程单独拎出来成章,以此学习,方便日后查看

性能优化 (渲染、请求、打包、代码)

  1. 性能指标

  2. lighthouse

  3. 优化资源请求

    1. 图像
      • 使用字体图标和svg标签
          <style> 
              background-image: url(‘src’)       <svg></svg>
          </style>
          <img src=’src’>
      
      • 使用响应式图像
          <img src=“src” srcset=“src 50w,src2 100w”>
      
      • 减小构建时的体积 Tree-shaking
      • 压缩资源 nginx中开启gzip对资源进行压缩
      • 合并请求
  4. vue性能优化

    1. 编码优化
      懒加载、异步组件、按需加载(babel-import-plugin)
      keep-alive
      Object.freeze()
      拆分组件
      数据持久化(防抖、节流)
      v-if v-show

    2. 加载性能优化
      第三方模块按需引入 element-ui
      vue-virtual-sroll-list
      vue-lazy

    3. 用户体验
      骨架屏

    4. SEO优化
      预渲染preender-spa-plugin
      服务端渲染ssr

    5. 打包
      cdn加载第三方模块
      抽离公共文件
      sourceMap生成

    6. 缓存、压缩

Webpack

  • loader
    文件转换;webpack只能解析js,loader使可以解析非js文件
    url-loader/file-loader  
    vue-style-loader/css-loader/less-loader/sass-loader // 样式转换  
    cache-loader/babel-loader/babel-core // es6转es5  
    eslint-loader  
    
  • plugin
    扩展webpack的功能(如打包优化、压缩)
    DefinePlugin // 定义环境变量 production/delvepment
    VueLoaderPlugin
    HotModuleReplacementPlugin // 热更新
    HtmlWebpackPlugin // 简化html文件创建,设置loading
    CommonChunkPlugin
    uglifyjs-webpack-plugin // 通过UglifyES压缩ES6代码
    webpack-parallel-uglify-plugin  // 多核压缩,提高压缩速度
    MiniCssExtractPlugin(npm install mini-css-extract-plugin --save-dev)// 原本打包过后是css-in-js,配置后可将CSS单独打包出来  
    CompressionWebpackPlugin  // 配置需要压缩的 .js/.css ,ngnix配允许加载压缩文件gzip_static on
    DllPluginDllReferencePlugin  // 拆包
    configureWebpack: (config) => {  // 分割代码块,不适合dll的放到这里
        config.optimization.splitChunks = {}
    }
    BundleAnalyzerPlugin(npm intall webpack-bundle-analyzer --save-dev) // 查看项目一共打了多少包,每个包的体积,每个包里面的包情况
    babel-plugin-import // 按需加载
    

webpack打包原理

根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,当 webpack 处理程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle

webpack构建流程

  1. 初始化参数:
    从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:
    用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:
    根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:
    从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:
    在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:
    根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:
    在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
graph LR
解析入口文件entry-->|"@babel/parser编译"|生成AST抽象语法树-->|"@babel/traveser"|递归遍历查找所有依赖模块-->|"@babel/core、@babel/preset-env"|生成浏览器可执行code-->重写require生成bundle文件

vue-cli结合webpack-cli配置参数输出

npx vue-cli-service inspect --mode development >> webpack.config.development.js
npx vue-cli-service inspect --mode production >> webpack.config.production.js

热更新HMR

vue-cli-service serve启动一个开发服务器 (基于 webpack-dev-server),并附带开箱即用的模块热重载 (Hot-Module-Replacement)
vue-cli-service build --report/--report-json 会根据构建统计生成报告,它会帮助你分析包中包含的模块们的大小

  • 热更新原理
    启动本地服务,当浏览器访问资源时做响应
    服务端和客户端使用websocket实现长连接
    webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译
    每次编译都会生成hash值、已改动模块的json文件、已改动模块代码的js文件
    编译完成后通过socket向客户端推送当前编译的hash戳
    客户端的websocket监听到有文件改动推送过来的hash戳,会和上一次对比
    一致则走缓存,不一致则通过ajax和jsonp向服务端获取最新资源
    使用内存文件系统去替换有修改的内容实现局部刷新

优化指南

目录
1. 优化工具  
    1.1. 编译进度  
    1.2. 编译速度  
    1.3. 打包体积  
2. 优化开发
    2.1. 自动更新
    2.2. 热更新
3. 加快构建速度
    3.1. 更新 webpac4 -> 5
    3.2. 缓存
    3.3. 减少loder、plugin应用范围
    3.4. 优化resolve(解析模块) 配置
    3.5. 多进程构建
    3.6. 区分 production/development 环境
    3.7. SourceMap 路径信息
4. 减小打包体积
    4.1. 代码压缩(js&css)
    4.2. 代码分离(模块&css)
    4.3. Tree Shaking 摇树
    4.4. CDN(手动上传字体、图片至网络节点)
5. 加快加载速度
    5.1. 按需加载
    5.2. 浏览器缓存
    5.3. CDN 缓存

一、优化效率的工具

准备工作:安装webpack插件,助力分析

1. 编译进度

安装:

npm i -D progress-bar-webpack-plugin

webpack.common.js 配置:

const chalk = require('chalk')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
module.exports = {
        plugins: [
        // 进度条
        new ProgressBarPlugin({
              format: `:msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`
        })
    ],
}

效果:

2. 编译速度

安装:

npm i -D speed-measure-webpack-plugin

webpack.dev.js 配置:

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
    // ...webpack config...
})

效果:    

3. 打包体积

安装:

npm i -D webpack-bundle-analyzer

webpack.prod.js 配置:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
    plugins: [
        // 打包体积分析
        new BundleAnalyzerPlugin()
    ],
}

效果:

二、优化开发

1. 自动更新

在每次编译代码时,手动运行 npm run build 会显得很麻烦。
webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:

  1. webpack's Watch Mode
  2. webpack-dev-server
  3. webpack-dev-middleware

webpack官方推荐的是 webpack-dev-server

2. 热更新

热更新 指的是,在开发过程中,修改代码后,仅更新修改部分的内容,无需刷新整个页面。

1. webpack-dev-server

使用 webpack 内置的 HMR 插件,更新 webpack-dev-server 配置。

module.export = {
    devServer: {
        contentBase: './dist',
        hot: true, // 热更新
    },
}

2. vue-hot-reload-api

Vue热更新插件

3. Q&A

Q:配置了 SpeedMeasurePlugin 后,热更新就无效了,会提示 runtime is undefined
A:仅在分析构建速度时打开 SpeedMeasurePlugin 插件,这里先关闭 SpeedMeasurePlugin 的使用,来查看热更新效果

三、加快构建速度

1. 更新版本

1.1. webpac4 -> 5

webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化

1.2. 包管理工具

将 Node.js 、package 管理工具(例如 npm 或者 yarn)更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。

yarn在生产环境下,自动编译、部署等流程自动化、持续集成过程中,更加安全的保证 package.json & package-lock.json 中依赖包的版本一致性。

较新版本:

  • webpack@5.46.0
  • node@14.15.0
  • npm@6.14.8

2. 缓存

2.1 cache

通过配置 webpack 持久化缓存 cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度。

简单来说,通过 cache: filesystem 可以将构建过程的 webpack 模板进行缓存,大幅提升二次构建速度、打包速度,当构建突然中断,二次进行构建时,可以直接从缓存中拉取。

webpack.common.js 配置方式如下:

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

引入缓存后,首次构建时间将增加 15%,二次构建时间将减少 90%

2.2 DLL ❌

webpack 官网构建性能 中看到关于 dll 的介绍:

使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。

dll 的相关配置相对复杂。在 github 找到 autodll-webpack-plugin 辅助配置 dll 的插件,结果上面直接写了

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

SO,不再配置 dll,cache 真香。

2.3 cache-loader ❌

cache-loader 同样不需要引入,webpack5 的 cache 已经实现了缓存。

3. 减少loader、plugin

每个的 loader、plugin 都有其启动时间。尽量少地使用工具,将非必须的 loader、plugins 删除。

3.1 指定include

webpack构建性能文档 为 loader 指定 include,减少 loader 应用范围,仅应用于最少数量的必要模块。

const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /.js$/,
                include: path.resolve(__dirname, 'src'),
                loader: 'babel-loader',
            },
        ],
    },
};

3.2 管理资源

使用 webpack 资源模块 asset module 代替旧的 assets loader(如 file-loader/url-loader/raw-loader 等),减少 loader 配置数量。

4. 优化resolve(解析模块) 配置

resolve 用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围。

4.1 alias

4.2 extensions

4.3 modules

4.4 symlinks

5. 多进程构建

假如某个 loader 的构建时间占据了整个构建过程的 50% 以上,那么有没有方法来加快 loader 的构建速度呢?

可以通过多进程来实现,试想将 loader 放在一个独立的 worker 池中运行,就不会阻碍其他 loader 的构建了,可以大大加快构建速度。

5.1 thread-loader

通过 thread-loader 将耗时的 loader 放在一个独立的 worker 池中运行,加快 loader 构建速度。

安装:

npm i -D thread-loader

webpack.common.js 配置:

module.exports = {
    rules: [{
        test: /.module.(scss|sass)$/,
        include: path.resolve('src'),
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              importLoaders: 2,
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  [
                    'postcss-preset-env',
                  ],
                ],
              },
            },
          },
          {
            loader: 'thread-loader',
            options: {
              workerParallelJobs: 2
            }
          },
          'sass-loader',
        ],
    }]
}

webpack 官网 提到 node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 thread-loader 时,需要设置 workerParallelJobs: 2

由于 thread-loader 引入后,每个 worker 都是一个独立的 node.js 进程,需要 0.6s 左右的时间的开销。 因此,我们应该仅在非常耗时的 loader 前引入 thread-loader。

5.2 happypack ❌

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

6. 区分 production/development 环境

在开发过程中,切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。

同样,在生产环境,也应避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件。

7. SourceMap 路径信息

四、减小打包体积

1. 代码压缩(js & css)

体积优化第一步是压缩代码,通过 webpack 插件,将 JS、CSS 等文件进行压缩。

1.1 JS压缩

使用 TerserWebpackPlugin 来压缩 JavaScript。

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

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

webpack.prod.js 配置:

const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
              parallel: 4,
              terserOptions: {
                parse: {
                  ecma: 8,
                },
                compress: {
                  ecma: 5,
                  warnings: false,
                  comparisons: false,
                  inline: 2,
                },
                mangle: {
                  safari10: true,
                },
                output: {
                  ecma: 5,
                  comments: false,
                  ascii_only: true,
                },
              },
            }),
        ]
    }
}
ParallelUglifyPlugin ❌

你可能有听过 ParallelUglifyPlugin 插件,它可以帮助我们多进程压缩 JS,webpack5 的 TerserWebpackPlugin 默认就开启了多进程和缓存,无需再引入 ParallelUglifyPlugin。

1.2 CSS压缩

使用 CssMinimizerWebpackPlugin 压缩 CSS 文件。

optimize-css-assets-webpack-plugin 相比,css-minimizer-webpack-plugin 在 source maps 和 assets 中使用查询字符串会更加准确,而且支持缓存和并发模式下运行。

CssMinimizerWebpackPlugin 将在 Webpack 构建期间搜索 CSS 文件,优化、压缩 CSS。

安装:

npm install -D css-minimizer-webpack-plugin

webpack.prod.js 配置:

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
          parallel: 4,
        }),
    ],
  }
}

由于 CSS 默认是放在 JS 文件中,因此本示例是基于下章节将 CSS 代码分离后的效果。

2. 代码分离(模块&css)

代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间。

2.1 抽离重复代码

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

通过 splitChunks 把 vue 等公共库抽离出来,不重复引入占用体积。

注意:切记不要为 cacheGroups 定义固定的 name,因为 cacheGroups.name 指定字符串或始终返回相同字符串的函数时,会将所有常见模块和 vendor 合并为一个 chunk。这会导致更大的初始下载量并减慢页面加载速度。

webpack.prod.js 配置:

module.exports = {
    splitChunks: {
      // include all types of chunks
      chunks: 'all',
      // 重复打包问题
      cacheGroups:{
        vendors:{ // node_modules里的代码
          test: /[\/]node_modules[\/]/,
          chunks: "all",
          // name: 'vendors', 一定不要定义固定的name
          priority: 10, // 优先级
          enforce: true 
        }
      }
    },
}

2.2 CSS 文件分离

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

安装:

npm install -D mini-css-extract-plugin

webpack.common.js 配置:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [new MiniCssExtractPlugin()],
  module: {
    rules: [
        {
        test: /.module.(scss|sass)$/,
        include: paths.appSrc,
        use: [
          'style-loader',
          isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境
          {
            loader: 'css-loader',
            options: {
              modules: true,
              importLoaders: 2,
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  [
                    'postcss-preset-env',
                  ],
                ],
              },
            },
          },
          {
            loader: 'thread-loader',
            options: {
              workerParallelJobs: 2
            }
          },
          'sass-loader',
        ].filter(Boolean),
      },
    ]
  },
};

注意:MiniCssExtractPlugin.loader 要放在 style-loader 后面。

2.3 最小化 entry chunk

通过配置 optimization.runtimeChunk = true,为运行时代码创建一个额外的 chunk,减少 entry chunk 体积,提高性能。

webpack.prod.js 配置:

module.exports = {
    optimization: {
        runtimeChunk: true,
      },
    };
}

3. Tree Shaking 摇树

顾名思义,就是将枯黄的落叶摇下来,只留下树上活的叶子。枯黄的落叶代表项目中未引用的无用代码,活的树叶代表项目中实际用到的源码。

3.1 JS

JS Tree Shaking 将 JavaScript 上下文中的未引用代码(Dead Code)移除,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供b标识,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

webpack5 sideEffects

通过 package.json 的 "sideEffects" 属性,来实现这种方式。

对组件库引用的优化

webpack5 sideEffects 只能清除无副作用的引用,而有副作用的引用则只能通过优化引用方式来进行 Tree Shaking

  1. lodash

    类似 import { throttle } from 'lodash' 就属于有副作用的引用,会将整个 lodash 文件进行打包。

    优化方式是使用 import { throttle } from 'lodash-es' 代替 import { throttle } from 'lodash'lodash-esLodash 库导出为 ES 模块,支持基于 ES modules 的 tree shaking,实现按需引入。

  2. ant-design

    ant-design 默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from 'antd' 就会有按需加载的效果。

    假如项目中仅引入少部分组件,import { Button } from 'antd' 也属于有副作用,webpack不能把其他组件进行tree-shaking。这时可以缩小引用范围,将引入方式修改为 import { Button } from 'antd/lib/button' 来进一步优化。

3.2 CSS

上述对 JS 代码做了 Tree Shaking 操作,同样,CSS 代码也需要摇摇树,打包时把没有用的 CSS 代码摇走,可以大幅减少打包后的 CSS 文件大小。

使用 purgecss-webpack-plugin 对 CSS Tree Shaking。

安装:

npm i purgecss-webpack-plugin -D

因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin 一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking。

webpack.prod.js 配置方式如下:

const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const paths = require('paths')

module.exports = {
  plugins: [
    // 打包体积分析
    new BundleAnalyzerPlugin(),
    // 提取 CSS
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
    // CSS Tree Shaking
    new PurgeCSSPlugin({
      paths: glob.sync(`${paths.appSrc}/**/*`,  { nodir: true }),
    }),
  ]
}

4. CDN(手动上传字体、图片至网络节点)

上述是对 webpack 配置的优化,另一方面还可以通过 CDN 来减小打包体积。

这里引入 CDN 的首要目的为了减少打包体积,因此仅仅将一部分大的静态资源手动上传至 CDN,并修改本地引入路径。

将大的静态资源上传至 CDN:

  • 字体:压缩并上传至 CDN;
  • 图片:压缩并上传至 CDN。

五、加快加载速度

1. 按需加载

通过 webpack 提供的 import() 语法 动态导入 功能进行代码分离,通过按需加载,大大提升网页加载速度。

因为现在使用 vue-cli3 脚手架创建的vue工程是使用 babel7 编译的,babel-plugin-component 已经不再适用于 babel7,文本将会使用 babel-plugin-import 实现按需引入的功能。其实 babel-plugin-importbabel-plugin-component 差异不大,只是做一下语法转换。

2. 浏览器缓存

浏览器缓存,就是进入某个网站后,加载的静态资源被浏览器缓存,再次进入该网站后,将直接拉取缓存资源,加快加载速度。

webpack 支持根据资源内容,创建 hash id,当资源内容发生变化时,将会创建新的 hash id。

配置 JS bundle hash,webpack.common.js 配置方式如下:

module.exports = {
  // 输出
  output: {
    // 仅在生产环境添加 hash
    filename: ctx.isEnvProduction ? '[name].[contenthash].bundle.js' : '[name].bundle.js',
  },
}

配置 CSS bundle hash,webpack.prod.js 配置方式如下:

module.exports = {
  plugins: [
    // 提取 CSS
    new MiniCssExtractPlugin({
      filename: "[hash].[name].css",
    }),
  ],
}

配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而改变,减少非必要的 hash 变动 webpack.prod.js 配置:

module.exports = {
  optimization: {
    moduleIds: 'deterministic',
  }
}

通过配置 contenthash/hash,浏览器缓存了未改动的文件,仅重新加载有改动的文件,大大加快加载速度。

3. CDN 缓存

将所有的静态资源,上传至 CDN,通过 CDN 加速来提升加载速度。

webpack.common.js 配置:

export.modules = {
output: {
    publicPath: ctx.isEnvProduction ? 'https://xxx.com' : '', // CDN 域名
  },
}