深刻浅出 webpack(vue 项目优化)

深刻浅出 webpack

最近在写一下对本身的思考,发现还有两篇草稿没发,记得应该是去年年初的时候写的,后来公司一直忙,就不多来社区了,今天先发一篇了,这篇记得当时还找到做者,加了微信css

webpack 项目优化

webpack 版本不一样,配置也会有一些地方不同的,这里是 webpack 4html

  • 1.优化构建速度。在项目庞大时构建耗时可能会变的很长,每次等待构建的耗时加起来也会是个大数目。
    • 4-1 缩小文件搜索范围
    • 4-2 使用 DllPlugin
    • 4-3 使用 HappyPack
    • 4-4 使用 ParallelUglifyPlugin
  • 2.优化使用体验。经过自动化手段完成一些重复的工做,让咱们专一于解决问题自己。
    • 4-5 使用自动刷新
    • 4-6 开启模块热替换
  • 3.优化输出质量 优化输出质量的目的是为了给用户呈现体验更好的网页,例如减小首屏加载时间、提高性能流畅度等。
    这相当重要,由于在互联网行业竞争日益激烈的今天,这可能关系到你的产品的生死。 优化输出质量本质是优化构建输出的要发布到线上的代码,分为如下几点:
    • 减小用户能感知到的加载时间,也就是首屏加载时间。
      • 4-7 区分环境
      • 4-8 压缩代码
      • 4-9 CDN 加速
      • 4-10 使用 Tree Shaking
      • 4-11 提取公共代码
      • 4-12 按需加载
    • 提高流畅度,也就是提高代码性能。
      • 4-13 使用 Prepack
      • 4-14 开启 Scope Hoisting
    • 优化的关键是找出问题所在,这样才能一针见血,
      • 4-15 输出分析 教你如何利用工具快速找出问题所在。
    • 4-16 优化总结 对以上的优化方法作一个总结

4-1 缩小文件搜索范围

  • 4-1-1 优化 loader 配置
    为了尽量少的让文件被 Loader 处理,能够经过 include 去命中只有哪些文件须要被处理。
    复制代码
  • 4-1-2 优化 resolve.alias 配置
  • 4-1-3 优化 resolve.extensions 配置

4-2 使用DllPlugin

用过 Windows 系统的人应该会常常看到以 .dll 为后缀的文件,这些文件称为动态连接库,
在一个动态连接库中能够包含给其余模块调用的函数和数据。vue

要给 Web 项目构建接入动态连接库的思想,须要完成如下事情:node

  • 把网页依赖的基础模块抽离出来,打包到一个个单独的动态连接库中去。一个动态连接库中能够包含多个模块。
  • 当须要导入的模块存在于某个动态连接库中时,这个模块不能被再次被打包,而是去动态连接库中获取。
  • 页面依赖的全部动态连接库须要被加载。

为何给 Web 项目构建接入动态连接库的思想后,会大大提高构建速度呢? 缘由在于包含大量复用模块的动态连接库只须要编译一次,
在以后的构建过程当中被动态连接库包含的模块将不会在从新编译,
而是直接使用动态连接库中的代码react

4-3 使用 HappyPack

分解任务和管理线程的事情 HappyPack 都会帮你作好webpack

4-4 使用 ParallelUglifyPlugin

用过 UglifyJS 的你必定会发如今构建用于开发环境的代码时很快就能完成,
但在构建用于线上的代码时构建一直卡在一个时间点迟迟没有反应,其实卡住的这个时候就是在进行代码压缩。git

因为压缩 JavaScript 代码须要先把代码解析成用 Object 抽象表示的 AST 语法树,
再去应用各类规则分析和处理 AST,致使这个过程计算量巨大,耗时很是多。github

为何不把在4-3 使用 HappyPack中介绍过的多进程并行处理的思想也引入到代码压缩中呢?web

ParallelUglifyPlugin 就作了这个事情。
当 Webpack 有多个 JavaScript 文件须要输出和压缩时,本来会使用 UglifyJS 去一个个挨着压缩再输出,
可是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工做分配给多个子进程去完成,
每一个子进程其实仍是经过 UglifyJS 去压缩代码,可是变成了并行执行。
因此 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工做。正则表达式

使用 ParallelUglifyPlugin 也很是简单,把原来 Webpack 配置文件中内置的 UglifyJsPlugin 去掉后,再替换成 ParallelUglifyPlugin,

不过看到 GitHub 上说是支持并行的uglifyjs-webpack-plugin/#parallel

4-5 使用自动刷新

要让 Webpack 开启监听模式,有两种方式: 在配置文件 webpack.*.config.js 中设置 watch: true。 在执行启动 Webpack 命令时,带上 --watch 参数,完整命令是 webpack --watch

文件监听工做原理: 在 Webpack 中监听一个文件发生变化的原理是定时的去获取这个文件的最后编辑时间,
每次都存下最新的最后编辑时间,若是发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。
配置项中的 watchOptions.poll 就是用于控制定时检查的周期,具体含义是每隔多少毫秒检查一次。

  • 优化文件监听性能
watchOptions: {
  // 不监听的 node_modules 目录下的文件
  ignored: /node_modules/,
}
复制代码

4-6 开启模块热替换

webpack 内置插件 HotModuleReplacementPlugin,
配置 devServer

// these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
复制代码

要优化模块热替换的构建性能,思路和在4-5 使用自动刷新中提到的很相似:
监听更少的文件,忽略掉 node_modules 目录下的文件。
可是其中提到的关闭默认的 inline 模式手动注入代理客户端的优化方法不能用于在使用模块热替换的状况下,
缘由在于模块热替换的运行依赖在每一个 Chunk 中都包含代理客户端的代码。

4-7 区分环境

4-8 压缩代码

要在 Webpack 中接入 UglifyJS 须要经过插件的形式,目前有两个成熟的插件,分别是:
UglifyJsPlugin:经过封装 UglifyJS 实现压缩。
ParallelUglifyPlugin:多进程并行处理压缩

  • 压缩 CSS

    把 cssnano 接入到 Webpack 中也很是简单,由于 css-loader 已经将其内置了,
    要开启 cssnano 去压缩代码只须要开启 css-loader 的 minimize 选项

const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,// 增长对 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          // 经过 minimize 选项压缩 CSS 代码
          use: ['css-loader?minimize']
        }),
      },
    ]
  },
  plugins: [
    // 用 WebPlugin 生成对应的 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路径
      filename: 'index.html' // 输出的 HTML 的文件名称
    }),
    new ExtractTextPlugin({
      filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 Hash 值
    }),
  ],
}
复制代码
  • 压缩 ES6

4-9 CDN 加速

以前的相对路径,都变成了绝对的指向 CDN 服务的 URL 地址,配置中的path 也须要换成 CDN 地址前缀

4-10 使用 Tree Shaking

Tree Shaking 能够用来剔除 JavaScript 中用不上的死代码。它依赖静态的 ES6 模块化语法

4-11 提取公共代码

Webpack 内置了专门用于提取多个 Chunk 中公共部分的插件 CommonsChunkPlugin,CommonsChunkPlugin 大体使用方法以下:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
  // 从哪些 Chunk 中提取
  chunks: ['a', 'b'],
  // 提取出的公共部分造成一个新的 Chunk,这个新 Chunk 的名称
  name: 'common'
})
复制代码

4-12 按需加载

router 按需加载

4-13 使用 Prepack

在前面的优化方法中提到了代码压缩和分块,这些都是在网络加载层面的优化,
除此以外还能够优化代码在运行时的效率,Prepack 就是为此而生。
Prepack 由 Facebook 开源,它采用较为激进的方法:
在保持运行结果一致的状况下,改变源代码的运行逻辑,输出性能更高的 JavaScript 代码。
实际上 Prepack 就是一个部分求值器,编译代码时提早将计算结果放到编译后的代码中,而不是在代码运行时才去求值。

Prepack 经过在编译阶段预先执行了源码获得执行结果,再直接把运行结果输出来以提高性能

  • Prepack 的工做原理和流程大体以下:

    经过 Babel 把 JavaScript 源码解析成抽象语法树(AST),以方便更细粒度地分析源码;
    Prepack 实现了一个 JavaScript 解释器,用于执行源码。
    借助这个解释器 Prepack 才能掌握源码具体是如何执行的,并把执行过程当中的结果返回到输出中。
    从表面上看去这彷佛很是美好,但实际上 Prepack 还不够成熟与完善。

  • Prepack 目前还处于初期的开发阶段,局限性也很大,例如:

    • 不能识别 DOM API 和 部分 Node.js API,若是源码中有调用依赖运行环境的 API 就会致使 Prepack 报错;
    • 存在优化后的代码性能反而更低的状况;
    • 存在优化后的代码文件尺寸大大增长的状况。
    • 总之,如今把 Prepack 用于线上环境还为时过早
  • 接入 Webpack

    const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
    
      module.exports = {
        plugins: [
          new PrepackWebpackPlugin()
        ]
      };
    复制代码

4-14 开启 Scope Hoisting

Scope Hoisting 可让 Webpack 打包出来的代码文件更小、运行的更快, 它又译做 "做用域提高",
是在 Webpack3 中新推出的功能

好处是:

  • 代码体积更小,由于函数申明语句会产生大量代码;

  • 代码在运行时由于建立的函数做用域更少了,内存开销也随之变小。

  • Scope Hoisting 的实现原理其实很简单: 分析出模块之间的依赖关系,尽量的把打散的模块合并到一个函数中去,但前提是不能形成代码冗余。
    所以只有那些被引用了一次的模块才能被合并。

因为 Scope Hoisting 须要分析出模块之间的依赖关系,所以源码必须采用 ES6 模块化语句,否则它将没法生效。
缘由和4-10 使用 TreeShaking 中介绍的相似。

4-15 输出分析

为了更简单直观的分析输出结果,社区中出现了许多可视化的分析工具。
这些工具以图形的方式把结果更加直观的展现出来,让你快速看到问题所在。

两种分析工具:

4-15-1 生成 stats.json

在启动 Webpack 时带上以上两个参数,启动命令以下:

webpack --profile --json > stats.json,
复制代码

若是没有问题,你会发现项目中多出了一个 stats.json 文件。
这个 stats.json 文件是给后面介绍的可视化分析工具使用的。

但是我在 vue 项目中使用时出现了一个问题

web>webpack --profile --json > stats.json
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory
.
Use --help to display the CLI options.
复制代码
  • 它的意思是,假如没有指定配置文件,会在当前目录寻找webpack.config.js 做为配置文件
  • 解决: 使用 config 指定配置文件,
    webpack --config ./build/webpack.dev.conf.js --json > stats.json
    复制代码

webpack --profile --json 会输出字符串形式的 JSON,
stats.json 是 UNIX/Linux 系统中的管道命令,
含义是把 webpack --profile --json 输出的内容经过管道输出到 stats.json 文件中。

4-15-2 官方的可视化分析工具: Webpack Analyse: 在线 Web 应用

打开 Webpack Analyse 连接的网页后,你就会看到一个弹窗提示你上传 JSON 文件,
也就是须要上传上面讲到的 stats.json 文件

4-15-3 webpack-bundle-analyzer

发现 vue-cli 2 版本中 webpack.prod.conf.js 里面有关因而否开启 webpack-bundle-analyzer 配置; 也就是说 npm run build --report 的时候,BundleAnalyzerPlugin 能以可视化的方式展现打包结果;

若是单独使用 webpack-bundle-analyzer:

  • 1.安装 webpack-bundle-analyzer 到全局,执行命令 npm i -g webpack-bundle-analyzer;
  • 2.按照上面提到的方法生成 stats.json 文件;
  • 3.在项目根目录中执行 webpack-bundle-analyzer 后,浏览器会打开对应网页看到以上效果

4-16 优化总结

按照开发环境和线上环境为该项目配置了两份文件,下面是使用 webpack4 版本

  • 侧重优化开发体验的配置文件 webpack.config.js:
const path = require('path');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');

// 自动寻找 pages 目录下的全部目录,把每个目录当作一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
  // HTML 模版文件所在的文件路径
  template: './template.html',
  // 提取出全部页面公共的代码
  commonsChunk: {
    // 提取出公共代码 Chunk 的名称
    name: 'common',
  },
});

module.exports = {
  // AutoWebPlugin 会找为寻找到的全部单页应用,生成对应的入口配置,
  // autoWebPlugin.entry 方法能够获取到生成入口配置
  entry: autoWebPlugin.entry({
    // 这里能够加入你额外须要的 Chunk 入口
    base: './src/base.js',
  }),
  output: {
    filename: '[name].js',
  },
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减小搜索步骤
    // 其中 __dirname 表示当前工做目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')],
    // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件,使用 Tree Shaking 优化
    // 只采用 main 字段做为入口文件描述字段,以减小搜索步骤
    mainFields: ['jsnext:main', 'main'],
  },
  module: {
    rules: [
      {
        // 若是项目源码中只有 js 文件就不要写成 /\.jsx?$/,提高正则表达式性能
        test: /\.js$/,
        // 使用 HappyPack 加速构建
        use: ['happypack/loader?id=babel'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
      {
        test: /\.js$/,
        use: ['happypack/loader?id=ui-component'],
        include: path.resolve(__dirname, 'src'),
      },
      {
        // 增长对 CSS 文件的支持
        test: /\.css$/,
        use: ['happypack/loader?id=css'],
      },
    ]
  },
  plugins: [
    autoWebPlugin,
    // 使用 HappyPack 加速构建
    new HappyPack({
      id: 'babel',
      // babel-loader 支持缓存转换出的结果,经过 cacheDirectory 选项开启
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      // UI 组件加载拆分
      id: 'ui-component',
      loaders: [{
        loader: 'ui-component-loader',
        options: {
          lib: 'antd',
          style: 'style/index.css',
          camel2: '-'
        }
      }],
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中同样
      loaders: ['style-loader', 'css-loader'],
    }),
    // 4-11提取公共代码
    new CommonsChunkPlugin({
      // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
  ],
  watchOptions: {
    // 4-5使用自动刷新:不监听的 node_modules 目录下的文件
    ignored: /node_modules/,
  }
};
复制代码
  • 侧重优化输出质量的配置文件 webpack-dist.config.js:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

// 自动寻找 pages 目录下的全部目录,把每个目录当作一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
  // HTML 模版文件所在的文件路径
  template: './template.html',
  // 提取出全部页面公共的代码
  commonsChunk: {
    // 提取出公共代码 Chunk 的名称
    name: 'common',
  },
  // 指定存放 CSS 文件的 CDN 目录 URL
  stylePublicPath: '//css.cdn.com/id/',
});

module.exports = {
  // AutoWebPlugin 会找为寻找到的全部单页应用,生成对应的入口配置,
  // autoWebPlugin.entry 方法能够获取到生成入口配置
  entry: autoWebPlugin.entry({
    // 这里能够加入你额外须要的 Chunk 入口
    base: './src/base.js',
  }),
  output: {
    // 给输出的文件名称加上 Hash 值
    filename: '[name]_[chunkhash:8].js',
    path: path.resolve(__dirname, './dist'),
    // 指定存放 JavaScript 文件的 CDN 目录 URL
    publicPath: '//js.cdn.com/id/',
  },
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减小搜索步骤
    // 其中 __dirname 表示当前工做目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')],
    // 只采用 main 字段做为入口文件描述字段,以减小搜索步骤
    mainFields: ['jsnext:main', 'main'],
  },
  module: {
    rules: [
      {
        // 若是项目源码中只有 js 文件就不要写成 /\.jsx?$/,提高正则表达式性能
        test: /\.js$/,
        // 使用 HappyPack 加速构建
        use: ['happypack/loader?id=babel'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
      {
        test: /\.js$/,
        use: ['happypack/loader?id=ui-component'],
        include: path.resolve(__dirname, 'src'),
      },
      {
        // 增长对 CSS 文件的支持
        test: /\.css$/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
          // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
          publicPath: '//img.cdn.com/id/'
        }),
      },
    ]
  },
  plugins: [
    autoWebPlugin,
    // 4-14开启ScopeHoisting
    new ModuleConcatenationPlugin(),
    // 4-3使用HappyPack
    new HappyPack({
      // 用惟一的标识符 id 来表明当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // babel-loader 支持缓存转换出的结果,经过 cacheDirectory 选项开启
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      // UI 组件加载拆分
      id: 'ui-component',
      loaders: [{
        loader: 'ui-component-loader',
        options: {
          lib: 'antd',
          style: 'style/index.css',
          camel2: '-'
        }
      }],
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中同样
      // 经过 minimize 选项压缩 CSS 代码
      loaders: ['css-loader?minimize'],
    }),
    new ExtractTextPlugin({
      // 给输出的 CSS 文件名称加上 Hash 值
      filename: `[name]_[contenthash:8].css`,
    }),
    // 4-11提取公共代码
    new CommonsChunkPlugin({
      // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
    new DefinePlugin({
      // 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才须要的部分
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      uglifyJS: {
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除全部的注释
          comments: false,
        },
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
          warnings: false,
          // 删除全部的 `console` 语句,能够兼容ie浏览器
          drop_console: true,
          // 内嵌定义了可是只用到一次的变量
          collapse_vars: true,
          // 提取出出现屡次可是没有定义成变量去引用的静态值
          reduce_vars: true,
        }
      },
    }),
  ]
};
复制代码

吴浩麟拥有本书的著做权。
其它人不能将本书用于商用用途,不能转载,不能以任何形式发行,违者将追究法律责任。

参考

相关文章
相关标签/搜索