webpack 构建速度和体积优化

构建速度优化

使用 webpack 内置的 stats

  • stats 构建的统计信息

  • package.json 中使用 stats

"scripts": {
  "build:stats": "webpack --env production --json > stats.json"
},
  • 缺点:颗粒度太粗,看不出问题所在。

速度分析

  • 使用 speed-measure-webpack-plugin 可以看到每个 loader 和插件执行耗时
  • 安装
yarn add speed-measure-webpack-plugin -D
  • 使用
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
 
const smp = new SpeedMeasurePlugin();
 
const webpackConfig = smp.wrap({
  plugins: [
    ...
  ]
});

分析体积大小 webpack-bundle-analyzer

  • 安装
npm install --save-dev webpack-bundle-analyzer
or
yarn add -D webpack-bundle-analyzer
  • 使用
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

使用高版本的 webpack 和 node.js

  • 因为 webpack 和 node 都在优化和迭代,使用高版本的可以有效优化打包构建速度

使用 webpack4 优化原因

  • v8 带来的优化(for of 替代 forEach、Map 和 Set 替代 Object,includes 替代 indexOf)
  • 默认使用更快的 md4 hash 算法
  • webpack AST 可以直接从 loader 传递给 AST ,减少分析时间
  • 使用字符串方法替代正则表达式

使用 多进程/ 多实例构建

使用 HappyPack 解析资源

  • 原理: 每次 webapck 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中。
const HappyPack = require('happypack');

exports.module = {
  loaders: {
    test: /.js$/,
    loader: 'happypack/loader',
    include: [
      // ...
    ],
  }
};

这个库作者已经不维护了,webpack4 后的推荐使用 thread-loader

使用 thread-loader 解析资源

  • 原理: 每次 webpack 解析一个模块,thread-loader 会将它及它的依赖分配给 worker 线程中
// 安装
npm install --save-dev thread-loader
// webapck.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          'thread-loader',
          // your expensive loader (e.g babel-loader)
        ],
      },
    ],
  },
};

使用 多进程/ 多实例 并行压缩

使用 parallel-uglify-plugin 插件

import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      uglifyJS: {
        output: {
          beautify: false,
          comments: false
        },
        comperss: {
          // 是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出,可以设置为false关闭这些作用
          warnings: false,
          drop_console: true,
          collapse_vars: true,
          reduce_vars: true
        }
      },
    }),
  ],
};

使用 uglifyjs-webpack-plugin 开启 parallel 参数

  • 安装
npm i -D uglifyjs-webpack-plugin
  • 使用
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJsPlugin({
      uglifyOptions: {
        warnings: false,
        parse: {},
        compress: {},
        mangle: true,
        output: null,
        toplevel: false,
        nameCache: null,
        ie8: false,
        keep_fnames: false
      },
      parallel: true
    })
  ]
}

terser-webpack-plugin 开启 parallel 参数

  • 推荐使用
module.exports = {
  optimization: {
    minimizer: [
      new Terserplugin({
        parallel: true
      })
    ]
  }
}

分包构建

使用 html-webpack-externals-plugin

  • 将基础包通过 cdn 引入,不打入 bundle 中
new HtmlWebpackExternalsPlugin({
  externals: [
    {
      module: 'react',
      entry: 'https://cdn.bootcdn.net/ajax/libs/react/17.0.0/cjs/react.production.min.js',
      global: 'React',
    },
  ],
})

缺点如果需要多个基础包,引入的还是过多

使用预编译资源模块

  • 思路:将 react react-dom redux 等基础包和业务基础包打成一个文件
  • 方法: 使用 DLLPlugin 进行分包,DllReferencePlugin 对 manifest.json 引用

使用

  • 创建 webpack.dll.config.js
module.exports = {
  context: process.cwd(),
  entry: {
    library: [
      'react',
      'react-dom',
      'redux',
      'react-redux'
    ],
    // 如果有多个,直接在增加一个
  }
  output: {
    // 这里打包后的文字是 library.dll.js
    filename: '[name].dll.js',
    path: path.resolve(__dirname, 'build/libarary'),
    // 暴露的库的名字
    library: '[name]'
  },
  plugins: [
    // 指定包存放的位置
    new webpack.DllPlugin({
      name: '[name]',
      // 描述动态链接库 mainfest 文件输出时的文件名称
      // path: 'manifest.json'
      path: path.resolve(__dirname, 'build/libarary/[name].json')
    })
  ]
}
    
  • 在 pageage.json scripts 中增加命令
"scripts": {
  "dll": "webpack --config webpack.dll.js"
}
  • 执行 npm run dll 分包
  • 在 webpack.config.js 引入
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      // 刚打包后的json文件地址
      // manifest: require('xxx.json'),
      manifest: require('./build/library/libary.json'),
    })
    // 如果引入多个,使用多次此插件
  ]
}

利用缓存提升构建速度

  • 目的:提升二次构建速度

  • 缓存思路

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  • 开启缓存后 node_modules 下会有一个 .cache 目录

babel-loader 开启缓存

use: [
  {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true
    }
  }
]

terser-webpack-plugin 开启缓存

module.exports = {
  optimization: {
    minimizer: [
      new Terserplugin({
        parallel: true,
        cache: true
      })
    ]
  }
}

使用 hard-source-webpack-plugin 开启缓存

  • 安装
npm install --save-dev hard-source-webpack-plugin or yarn add --dev hard-source-webpack-plugin
  • 使用
// webpack.config.js

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

plugins: [
  new HardSourceWebpackPlugin()
]

缩小构建目标来优化构建速度

  • 目的:尽可能的少构建模块
    • 如: babel-loader 不解析 node_modules

减少文件搜索范围

合理使用 resolve.alias

  • 创建 import 或 require 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块
module.exports = {
  resolve: {
    alias: {
      react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
    }
  }
}

resolve.modules 配置 (减少模块搜索层级)

  • 告诉 webpack 解析模块时应该搜索的目录。
module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, "node_modules")]
  }
}

resolve.mainFields

  • 告诉 webpack 入口文件,此选项将决定在 package.json 中使用哪个字段导入模块。
module.exports = {
  resolve: {
    //指定入口文件 main 意思是从 package.json 中 main 字段中查找
    mainFields: ['main']
  }
}

resolve.extensions

  • 自定义解析确定的扩展。默认值是:
  • 模块路径的查找,例如: import Index from 'index' 没有写后缀,webpack 会先去找 index.js 文件,在去找 index.json
extensions: [".js", ".json"]
  • 可以避免 webpack 做没意义的查找

构建体积优化

Tree Shaking

  • 概念: 1 个模块可能有多个方法,只要其中某个方法使用到了,则整个问价你都会被打包到 bundle 里面去,tree shaking 就是只把用到的方法打入 bundle,没有到的会被 uglify 阶段被擦除掉。

无用的 CSS 如何删除掉的?

  • PurifyCSS:遍历代码,识别已经用到的 CSS Class (已经不维护了)

  • uncss:HTML 需要通过 jsdom 加载,所有的样式通过 PostCss 解析,通过 document.querySelector 来识别在 HTML 文件里面不存在的选择器。

  • 使用 purgecss-webpack-plugin 和 mini-css-extract-plugin 配合使用

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const PATHS = {
  src: path.join(__dirname, 'src')
}

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader,
        "css-loader"
      ]
    }
  ]
},
plugins: [
  new MiniCssExtractPlugin({
    filename: "[name].css",
  }),
  new PurgeCSSPlugin({
    paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
  }),
]

图片压缩

  • 基于 Node 库 imagemin
  • 使用 配合 image-webpack-loader

imagemin 的优点

  • 有很多定制选项
  • 可以引入更多第三方插件,如 pngqeant
  • 可以处理多种图片格式

图片压缩的原理 png

  • pngquant: 是一款 PNG 压缩器,通过将图像转换为具有 alpha 通道 (通常比24/32位 PNG文件小 60%-80%)的更高效的 8 位 PNG 格式,可以显著减小文件大小。
  • pngcrush: 主要目的是通过尝试不同的压缩级别和 PNG 过滤方法来降低 PNG IDAT 数据流的大小。
  • optipng: 设计灵感来自于 pngcrush。optipng 可以将图像文件重新压缩更小尺寸,而不会丢失任何信息
  • tinypng: 也是将 24 位 PNG 文件转化为更小有索引的 8 位图片,同时所有非必要的 metadata 也会被剥离掉。

使用 image-webpack-loader

npm install image-webpack-loader --save-dev
// webpack.config.js
rules: [{
  test: /\.(gif|png|jpe?g|svg)$/i,
  use: [
    'file-loader',
    {
      loader: 'image-webpack-loader',
      options: {
        mozjpeg: {
          progressive: true,
        },
        // optipng.enabled: false will disable optipng
        optipng: {
          enabled: false,
        },
        pngquant: {
          quality: [0.65, 0.90],
          speed: 4
        },
        gifsicle: {
          interlaced: false,
        },
        // the webp option will enable WEBP
        webp: {
          quality: 75
        }
      }
    },
  ],
}]

动态 Polyfill

Polyfill 几种方案的优缺点

  1. babel-polyfill
  • 优点: React16官方推荐
  • 缺点:体积过大,难以抽离Map、Set。项目中 react 是单独用 CND 引入,使用时需要在 react 前加载。
  1. babel-plugin-transform-runtime
  • 优点:只能 polyfill 用到的类和方法,体积相对较小
  • 缺点:不能 polyfill 原型上的方法,不适用于业务复杂项目。
  1. 自己map、set的polyfill
  • 优点:定制化高,体积小
  • 缺点:就算体积小,但是所有用户都需要加载,重复造轮子
  1. polyfill-service
  • 优点: 识别 User Agent,只给用户返回需要的 polyfill
  • 缺点:可能国内部分奇葩浏览器 UA 无法识别 (但可以降级返回所需全部)

使用

  1. polyfill.io 官方提供的服务
https://polyfill.io/v3/polyfill.min.js
  1. 基于官方自建 polyfill 服务
https://polyfill.io/v3/polyfill.min.js?features=es2015%2Ces2016%2Ces2017
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容