头图

webpack 性能调优报告,极限压缩 monaco 编辑器

小磊

调优成果

优化前优化后优化比例
打包体积32.01 MB13.57 MB57.61%
Gzip 体积3.37 MB1.47 MB MB56.38%
文件数量821976.83%
资源压缩服务器 br 压缩客户端 br 压缩1
压缩级别611(最高)1
构建速度60s +41.1s +31.67%
播放页 FP 耗时明显感知无感知1

(以上数据来源为本地测试,仅供参考)

优化目标

  • 降低播放页 FP 耗时
  • 减少静态资源请求数量
  • 提升开发体验
  • 提升构建产物质量
    (文章基于 [email protected]

    问题分析

    先看项目当前的打包分析结果

以上是用 vue-cli 生成的构建统计报告,它会帮助分析包中包含的模块们的大小,简单列一下报告信息如下:

  1. 整体打包体积 32.01 MB
  2. ts.worker.js 是 monaco 编辑器的语言支持文件,主要提供 typescript 语法支持,体积 10.92 MB
  3. my-details.js 是播放页文件,网站核心资源文件,体积 6.81 MB
  4. chunk-vendors.js 第三方模块捆绑包,体积 4.14 MB
  5. html.workar.js 是 monaco 编辑器的 html 语法支持文件
  6. css.workar.js 是 monaco 编辑器的 css 语法支持文件
  7. json.worker.js 是 monaco 编辑器的 json 语法支持文件
  8. app.js 项目入口文件,体积 1.02 MB
  9. 小文件文件数量太多,加起来接近百个,导致 http 请求过多

经过分析总结,定位问题如下:

  1. monaco-editor 是最大的问题,体积占据半壁江山,严重影响加载速度,需要优化
  2. chunk-vendors.js 作为公共模块,构成项目必不可少的一些基础类库,升级频率都不高,但每个页面都需要它们,现在它体积过大,应该在合理范围内拆分成更小一些的 js,以利用浏览器的并发请求,优化首页加载体验。其中包含了三个大家伙:elementUI、moment 和 lodash,更是需要单独做优化
  3. my-details.js 和 app.js 作为项目主要的资源文件,体积过大,需要优化
  4. 小文件数量太多,需要合并

解决思路

检查下有没有哪些产出是不必要的,在有限的时间空间和算力下,去除低效的重复(提出公共大模块),进行合理的冗余(小文件允许重复),达到时间和空间综合考量上的最优。
下面分步骤实现每一个优化项。

monaco-editor 优化

缩小体积

我们业务层面,只需要用到展示功能,其语言编辑功能完全用不到,因此可以把语言包全部过滤掉,这里需要用到 monaco-editor-webpack-plugin 插件,配置添加插件选项 languages 为支持的语言数组(具体语言查看官网)默认是支持所有语言的,配置此项应该只是去除一些语言的高级特性支持。

new MonacoWebpackPlugin({
    languages: [],
}),

再次打包后,体积缩小至 9.26 MB。

单独打包

monaco-editor 作为一个重量级组件,会分散很多小文件到各个地方,从而增加文件数量和体积,进而造成流量损失,通过 webpack 的 splitChunks 功能拆分 monaco-editor 为单独独立文件,充分利用浏览器缓存,减少多次引用时的消耗。

splitChunks: {
  chunks: 'all',
  minSize: 20000,
  maxAsyncRequests: 30,
  maxInitialRequests: 30,
  enforceSizeThreshold: 50000,
  maxSize: 0,
  cacheGroups: {
    monacoEditor: {
      chunks: 'async',
      name: 'chunk-monaco-editor',
      priority: 22,
      test: /[\/]node_modules[\/]monaco-editor[\/]/,
      enforce: true,
      reuseExistingChunk: true,
    },
}

按需加载

通过按需加载,减少核心内容渲染前的阻塞,转到需要的时候加载(当前仅需要editor.api 和 js css高亮功能)。

async initEditor() {
  const monaco = await import(
    /* webpackPrefetch: true;" */'monaco-editor/esm/vs/editor/editor.api.js'
  );
  // 引入高亮模块
  await this.highlightLang()
}

// 引入语言高亮模块
highlightLang() {
  const LANGUAGES = ['javascript', 'css', 'html', 'mysql', 'java', 'python', 'markdown', 'go', 'lua'];
  const promiseList = [
    import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'),
    import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/css/css.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/html/html.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/mysql/mysql.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/java/java.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/python/python.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/go/go.contribution'),
    // import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/lua/lua.contribution'),
  ];
  return Promise.all(promiseList);
}

开启 prefetch

按需加载后,因为 monaco-editor 本身体量很大,因此在加载编辑器时会出现长时间无响应现象,采用 prefetch 预加载方案,在浏览器空闲时预先下载资源,到用的时候直接取,有效避免无响应情况。

const monaco = await import( /* webpackPrefetch: true;" */ 'monaco-editor');

moment.js 优化

尝试删除 moment.js 语言包后体积依然很大,最后采用和 moment.js api 完全兼容的 dayjs 替换,gzip压缩后仅仅 2kb

(因替换工作为全局,可能会出现和 moment.js 相关功能的 bug )

element-ui 优化

单独打包

理论上 UI 组件库也可以放入 chunk-vendors.js 中,但它实在是过大,可能比 libs 里所有的包加起来还要大不少,而且 UI 组件库的更新频率也相对比 chunk-vendors.js 要更高一点。Element-UI 组件库作为 UI 组件,应从 chunk-vendors.js 中分离出来,单独打包为 chunk-elementUI.js,如图所示打包后体积为 1.67 MB

按需引入

按照官方按需引入方式,只引入使用的组件,减少体积。

lodash.js 优化

按需引入

使用 webpack 插件 lodash-webpack-plugin 和 babel 插件实现按需打包 lodash

const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
config.plugin('loadshReplace').use(new LodashModuleReplacementPlugin());
module.exports = {
  presets: ['@vue/cli-plugin-babel/preset'],
  plugins: [
    'lodash',
  ],
};

优化后,lodash 基本上做到无感知存在

svgIcon优化

单独打包

svgIcon 组件库作为高频更新且引用超多的库,应单独分出为 chunk-svgIcon.js,如图所示打包后体积为 561 kb

删除 use-zh.svg

经过代码审查发现体积最大的 use-zh.svg 在项目中现在并未使用,所以删除

雪碧图

将所有 svg 合成雪碧图,减少请求次数,使用 svg-sprite-loader 实现

const svgRule = config.module.rule('svg');
 // 清除已有的所有 loader,如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
svgRule.uses.clear();
 // 添加要替换的 loader
svgRule
  .test(/.svg$/)
  .include.add(path.resolve(__dirname, 'src/components/svgIcon/svg'))
  .end()
  .use('svg-sprite-loader')
  .loader('svg-sprite-loader')
  .options({
    symbolId: 'icon-[name]',
  })
  .end();

压缩 svg (未执行)

因为压缩过后的 svg 图会存在去掉 fill 的情况,因此这一步骤并未执行

代码压缩

采用 terser 插件(webpack4 官方推荐)进行 js 和 css 代码压缩,同时去掉生产环境的注释和 console 信息

config.optimization.minimizer('terser').tap(options => {
    const compress = {
      warnings: false,
      drop_console: true,
      drop_debugger: true,
      pure_funcs: ['console.log'],
    };
    const initCompress = options[0].terserOptions.compress;
    options[0].terserOptions.compress = { ...initCompress, ...compress };
    return options;
});

图片压缩(未执行)

使用 webpack 插件 image-webpack-loader 对所有图片进行压缩,但是该插件依赖于系统环境,在不同环境下可能出现安装失败,编译失败等情况,我们项目目前图片资源较少,所以暂时不用。

开启 br 压缩

当前 br 压缩是在 nginx 服务器进行且并未缓存资源,因此每次请求都需要对资源进行压缩之后发出,考虑服务器性能,压缩级别设置为 6。
现在改为客户端压缩,服务器在接收请求时直接把压缩文件发出,减少服务器压力。同时客户端压缩可以把压缩级别调整至最高的 11,整体资源大小会再次下降,使用 compression-webpack-plugin 实现。

const CompressionWebpackPlugin = require('compression-webpack-plugin');
if (!isDev) {
  plugins.push(
    new CompressionWebpackPlugin({
      filename: '[path].br[query]',
      algorithm: 'brotliCompress',
      test: /.(js|css|json|txt|html|ico|svg)(?.*)?$/i,
      compressionOptions: {
        params: {
          [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
        },
      },
      threshold: 1024,
      minRatio: 0.99,
      //删除原始文件只保留压缩后的文件
      deleteOriginalAssets: false,
    }),
  );
}

多线程执行 loader

开启 parallel 为 Babel 或 TypeScript 使用 thread-loader

parallel: require('os').cpus().length > 1

打包缓存

采用 HardSourceWebpackPlugin 插件为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source。配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。
可能带来的问题,修复办法在这 hard-source-webpack-plugin
下面链接用于解决 hash 丢失问题
https://github.com/mzgoddard/...

其他修改项

  • 提取环境变量 const` isDev = process.env.NODE_ENV === 'development'; `
  • 增加打包速度检测插件 SpeedMeasurePlugin

最终报告

报告图:

阅读 2.9k

Node全栈
初心不改,砥砺前行,专注于原创高质量内容,只写干货,即是分享亦是记录,希望跟广大开发者一起进步,...

以一颗更加开放,更加多元,更加包容的心走进别人的世界

345 声望
879 粉丝
0 条评论

以一颗更加开放,更加多元,更加包容的心走进别人的世界

345 声望
879 粉丝
文章目录
宣传栏