阅读 3412

前端性能优化实战——记一次vue-cli3项目的优化过程

最近做了一个vue-cli项目的性能优化,这次也是把很多之前学过的知识应用在实际项目中,遇到了很多困难也学到了很多东西。

观看本文之前你最好对常用的前端优化方法(如http缓存、代码压缩等),以及webpack有一定了解

项目简介

这是一个用vue-cli 3作为脚手架的PC端web应用,全局多页面局部单页面(根据用户角色划分)。基本上使用的vue-cli的默认配置,也没有专门做过性能优化。这次优化希望在尽量不降低后续运行性能的前提下,加快首次加载的速度。

性能指标

做性能优化之前,我们得先知道如何评价一个项目的性能,也就是用哪些指标来衡量,这样才好设定一个目标,量化项目的性能,并且可以知道我们的性能优化有没有效果,有多大效果。

目前,前端性能监控的方式有两类,一类是合成监控(Synthetic Monitoring,SYN),另一类是真实用户监控(Real User Monitoring,RUM)。 合成监控是在一个模拟环境里,通过一些工具、规则去模拟运行一个页面,记录相关性能指标,最后得到一份报告。chrome 浏览器开发者工具自带的Lighthouse(需要科学上网)就是一种合成监控。但是,合成监控产生的数据量较小,且测试结果与测试机的网络状况、硬件性能等关系很大,无法代表真实用户使用的情况。RUM监控是同在代码里埋点、调用浏览器接口等方式,收集线上用户的实际性能数据,样本量大,且更能说明真实场景下页面的性能状况。因此本次性能优化时以RUM监控的数据为准。

RUM 性能指标,具体又分为用户体验指标和技术性能指标两类。

用户体验指标

基于以用户为中心的性能指标,常用的用户体验指标有:

指标描述具体计算方式
FP(First Paint)页面首次绘制时间(白屏时间)用户从开始访问页面到屏幕不再是白屏的时间。根据W3C Paint Timing 规范草案,在浏览器中可以通过performance.getEntriesByType('paint')获取 PerformancePaintTiming API 提供的打点信息。
FCP(First Contentful Paint)页面首次有内容绘制的时间用户从开始访问页面到有任何实际内容的时间。在浏览器中可以通过 performance.getEntriesByType('paint') 获取。
FMP(First Meaningful Paint)页面首次有效绘制时间(首屏时间)用户从开始访问页面到整体页面的最大布局变动之后的那个绘制的时间。Time to First Meaningful Paint(需要科学上网)
TTI(Time To Interactive)页面完全可交互时间用户从开始访问页面到页面处于完全可交互状态的时间。First Interactive and Consistently Interactive(需要科学上网)
FID(First Input Delay)用户首次交互操作的延时用户和页面进行首次交互操作所花费的时间。借助 Event Timing API ,监听 first-input 事件,FID=事件的开始处理时间-事件的发生时间
MPFID(Max Potential First Input Delay)用户交互操作可能遇到的最大延时用户和页面进行首次交互操作可能花费的最长时间。借助 Long Tasks API ,MPFID=耗时最长的Long Task
LOAD页面完全加载的时间(load 事件发生的时间)LOAD = loadEventStart - fetchStart

FP、FCP、FMP、TTI图示 图片来自www.jianshu.com/p/456e6eff5…

技术性能指标

技术性能指标是页面加载的过程中通过各事件的时间计算得到的性能指标,它对用户的影响没有用户体验指标直观,但是更能反应一些性能问题,也更容易针对性优化。而且与各公司有不同定义的用户体验指标不同,它有十分明确和统一的计算方式。

Navigation Timing 2.0 定义的页面加载阶段模型: Navigation Timing 2.0 页面加载阶段模型 根据 Navigtaion Timing 2.0 模型,有如下常用的技术性能指标:

指标描述具体计算方式
DNSDNS查询耗时domainLookupEnd - domainLookupStart
TCP建立TCP连接耗时connectEnd - connectStart
SSL建立SSL连接耗时connectEnd - secureConnectionStart
TTFB首字节响应时间responseStart - requestStart
内容传输Response传输耗时responseEnd - responseStart
DOM解析浏览器解析Dom耗时domInteractive - responseEnd
资源加载外部资源加载耗时loadEventStart - domContentLoadedEventEnd
首字节浏览器首次得到响应耗时responseStart - fetchStart
DOM ReadyDOM加载完毕耗时domContentLoadedEventEnd - fetchStart

本次优化关注的主要指标

由于这次优化的项目是以用户操作为主的应用,而不是以展示为主(比如新闻网站),因此弱化了对FP、FMP等指标的关注,主要关注TTI。次要关注FCP、FMP、DOM Ready,因为FCP、FMP的优化可以一定程度降低用户因为没有看到内容失去耐心而流失的情况,DOM Ready可以反应项目整体的加载时间。

本次优化的主要目标是TTI降低30%

分析工具

  • webpack-chart: webpack stats 可交互饼图
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的
  • webpack-bundle-analyzer: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户。在vue-cli中,运行npx vue-cli-service build --report/dist下生成report.html来查看打包分析
  • 运行npx vue-cli-service inspect > output.js --mode production生成webpack生产环境配置文件
  • 调试webpack插件:node --inspect-brk ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve --inline --progress
  • 全局安装@vue/cli后,运行vue ui可视化展示依赖结构
  • Lighthouse:Chrome开发者工具自带的性能报告工具,可以给出很多建议

vue-cli 已经默认做了的优化

vue-cli默认配置已经添加了很多常用的优化,包括:

  • cache-loader 会默认为 Vue/Babel/TypeScript 编译开启。文件会缓存在 node_modules/.cache
  • 图片等多媒体文件使用url-loader,小于4K的图片会转base64编码内联在js文件中
  • 生产环境下使用mini-css-extract-plugin,将css提取成单独的文件
  • thread-loader 会在多核 CPU 的机器上为 Babel/TypeScript 转译开启并行处理。
  • 提取公共代码:两个缓存组chunk-vendorschunk-common
  • 代码压缩(terser-webpack-plugin)
  • preload-webpack-plugin:所有入口js、css文件加上preload,按需加载文件加上prefetch

检查项目中哪些地方还有优化空间

所谓优化其本质就是少做多余的事,因此我们得知道哪些是必要的哪些是不必要的。比如在用户还没滑动到网页下方的时候就加载完整个网页,在多数情况下是不必要的。因此要知道项目运行的完整过程中的完整行为。这一点经常被忽略,而难以获得比较好的效果。

在开始做优化之前我们先检查一些项目里有哪些地方还有优化空间,避免做无用功,或者一顿操作猛如虎,一看数据心里苦。

静态资源

  • 图片:头像、封面还有压缩空间,现在平均100K左右,大的超过1M
  • ajax返回的json:目前平均每个ajax 1K左右,压缩空间不大
  • js、css:首屏加载的几个主要的加起来有几M,可以分割代码减少加载量
  • video、font、doc:加起来不超过50K,不影响性能,没有压缩必要
  • other:prefetch的文件过多,占用过多服务器带宽

TTFB

  • ajax:200-800ms
  • js、css:150-800ms,大多数700+ms (后来发现其实是我开了代理,实际是20-100ms)
  • 图片、video、font:50-250ms
  • doc:60ms

其它

  • 有些地方可以加缓存,减少ajax请求量,可以考虑用什么形式(session storage、cache storage、index db、http 缓存)
  • 部分页面可以考虑文档结构在服务端渲染
  • 有些地方有不必要的重绘

可能有效的方法

优化尝试

分片优化

分片(splitChunks)是webpack分割代码的方式,可以参考官网或者这篇博客

优化前:

原vue-cli默认配置(可以通过npx vue-cli-service inspect > output.js --mode production导出output.js查看当前配置):

splitChunks: {
  cacheGroups: {
    vendors: {
      name: 'chunk-vendors',
      test: /[\\/]node_modules[\\/]/,
      priority: -10,
      chunks: 'initial'
    },
    common: {
      name: 'chunk-common',
      minChunks: 2,
      priority: -20,
      chunks: 'initial',
      reuseExistingChunk: true
    }
  }
},
复制代码

各参数的含义这里不再解释,可以参考前面的链接

打包后几个主要入口大小:7-8M

入口(entry)定义了webpack打包代码后可以可以从哪开始执行代码,多页面的应用一般是多入口

这里简要说一下 Webpack打包和分片的执行方式:

  1. 对于所有入口,从入口文件开始为一个chunk,其import(不包括动态importrequire)的所有文件加入chunk,再把新加进来的文件所依赖的文件加入,也就是依赖链中的所有文件。这些是initialChunk。
  2. 前一步中遇到的所有动态import的文件各自为一个chunk。这些是asyncChunk。
  3. 提取公共代码,即cacheGroups,会产生新的chunk。根据配置中的规则,把多个chunk中重复文件提取出来成为新的chunk。
  4. 每个chunk中同一类型的文件合并为一个。

打包时的输出 例如上图是执行到某时刻时webpack的输出,其中 767/833 指已经编译好了767个模块,通过这767个模块已知要编译的模块有833个,随着下一个模块被编译,该模块的依赖也会被加入todo。因此左边数字在涨的同时右边数字也在涨,直到编译完所有模块。所以最左边的65%其实并不是指编译总进度,在编译完之前根本不知道一共有多少个模块。

在修改cacheGroups之前,先通过可视化工具看一下目前的分片情况,这是运行打包分析后的可视化分片结果:

分片情况

可以看到chunk-vendors体积非常大,这是因为项目依赖了很多库。chunk-common体积也非常大,这是因为所有入口的公共文件都会被打包进这个chunk。这两个chunk是所有入口都会加载的,所以比较影响性能。还出现了很多重复代码,和一些不常用的代码被打包进入口chunk。下面将会针对这些问题“开刀”。

修改分片的原则

  1. node_modules 中的文件还是打包进 chunk-vendors,被引用得太多,已经难以改成按需加载,而且变动频率低,可以作为浏览器缓存(304)长期不失效。
  2. 被引用少且体量大的文件单独分一个chunk。
  3. 其它公共代码分成多个chunk,这样可以避免从某个入口访问时下载全部公共代码,以及部分代码变动时不会导致全部公共代码的缓存失效。
  4. 体量很小的异步chunk合并进其它相关异步chunk中,或者合并进入口chunk。

路由懒加载

入口chunk太大,看了一下路由有很多都是静态import。改了一波入口动态import,只保留常用的几个路由是静态的。

路由懒加载vue-router提供的特性,可以使用const Foo = () => import('./Foo.vue')来按需加载Foo模块。其每个按需加载的模块会被分割成一个异步chunk,在实际调用Foo模块时,才会在当前DOM的<head><body>末尾插入<link><script>下载该chunk的文件。

路由修改成asyncChunk的原则:

使用频率高使用频率低
文件小合并进入口chunk单独一个chunk
文件大单独一个chunk(加prefetch)单独一个chunk

把大的入口chunk分割成小chunk的好处:减少用户首次访问时需要下载的文件大小,以及执行的代码量。减少不同入口间的重复代码。

坏处:用户后续操作可能就得再下载新文件,增加等待时间。异步chunk可能会依赖一些该入口chunk中就有的文件,导致下载重复代码。

根据路由划分,符合上述规则的每个路由一个chunck,其余保留直接静态import,或者合并进其它chunk(同名chunk会合并)

{
  path: '/user/XXX',
  name: 'user-XXX',
  // 改成动态import
  component: () => import(
    /* webpackChunkName: "chunk-user-XXX" */
    'src/pages/XXX'
  ),
},
复制代码

改完以后多了一些小chunk,但影响不大,都是异步加载的chunk,不会造成单次请求文件数过多而降低性能。

使用频率低的库单独分一片

  • tinymce,一个富文本编辑器控件为例。体积大的模块,可以改成按需加载。
const tinymce = () => import(
  /* webpackChunkName: "chunk-tinymce" */
  '../plugins/tinymce'
);
...
beforeMount() {
  // 动态import tinymce,在第一次mount之前再下载这个chunk(压缩后300K)
  tinymce().then(() => {
    this.loading = false;
  });
},
复制代码

经过这两步后,chunks总大小:16M => 10.8M(parsed)

分片情况: 两步后分片情况

可以看到重复代码少了很多,但是两个大chunk还是很大。继续分割。

  • echarts(672K):一个图表控件库,不是所有入口都用得到,但是被用的地方太多了,已经难以改成按需加载了
// 对vue.config.js进行修改
cacheGroups: {
  // 增加一个cacheGroup
  echarts: {
    name: 'chunk-echarts',
    test: /[\\/]node_modules[\\/]echarts[\\/]/,
    priority: 0,
    chunks: 'all',
  },
}
复制代码

增加公共代码分片数量

现在入口chunck还是太大,可以增加提取的公共chunck数量。提高会使重复代码变少,但是文件数会增加,所以也不是越高越好。

splitChunks: {
  maxInitialRequests: 5, //默认3
  maxAsyncRequests: 6, //默认5
  ...
},
复制代码

vendors提取

默认的策略是提取入口chunck依赖的所有node_modules下的文件,这有个问题,async的chunck又会把node_modules下的文件再打包一次,所以有一些库浏览器会下载两份。而且只被引用了一次的库没有提取的必要,即不减小chuncks的总大小,也不减少文件数,反而增加了chunk-vendors的大小,使得所有入口总文件大小增加。

cacheGroups: {
  ...
  vendors: {
    name: 'chunk-vendors',
    test: /[\\/]node_modules[\\/]/,
    priority: -10,
    // 增加下面两行
    minChunks: 2,
    chunks: 'all',
  },
}
复制代码

踩坑

修改分片规则后dom中的js文件有部分缺失:HtmlWebpackPlugin 的chunks插入方式

chunk-common分割

原本所有入口chunck的代码公共部分(至少引用2次)打包成一个chunk,但其实各个角色的用户不太可能访问别的入口,而所有入口都必须下载chunk-common,因此将chunk-common分割成多份(不再合并所有公共代码为一个文件)

common: {
  name: true, // 默认是name: 'chunk-common'
  minChunks: 2,
  minSize: 60000, // 大小超过60kb的模块才会被提取,防止一堆小chunck
  priority: -20,
  chunks: 'initial',
  reuseExistingChunk: true,
},
复制代码

经过这些处理后,chunks总大小:10.8M => 15.8M(parsed)

看上去总大小变大了,因为重复代码多了,但是各入口所需加载的文件从7-8M减少到了3-4M,平均减少了60%。和这比起来,多占用服务器5M存储空间根本无关紧要。

为什么把 chunk-common 分割开,却不把 chunk-verdors 分割开?

前面也提到了 chunk-verdors 是项目依赖的库,变更频率低,可以利用304缓存,如果分割开又会增加文件数量和首次加载时的请求数。而chunk-common里是项目本身的代码,经常变动,因此缓存经常失效,用户每次都得重新下载。

代码最小化

vue-cli 默认配置了terser进行代码压缩,可以修改其默认配置进一步压缩代码。

terser 会通过简化表达式和函数等方式来减小代码体积,和加快运行时的速度、减少运行时内存占用。

vue.config.js进行如下修改:

// webpack-chain 的用法见:https://github.com/neutrinojs/webpack-chain
chainWebpack: config => {
  if (process.env.NODE_ENV !== 'development') {
    config.optimization.minimizer('terser').tap((args) => {
      args[0] = {
        test: /\.m?js(\?.*)?$/i,
        chunkFilter: () => true,
        warningsFilter: () => true,
        extractComments: false, // 注释是否单独提取成一个文件
        sourceMap: true,
        cache: true,
        cacheKeys: defaultCacheKeys => defaultCacheKeys,
        parallel: true,
        include: undefined, // 对哪些文件生效
        exclude: undefined,
        minify: undefined, // 自定义minify函数
        // 完整参数见 https://github.com/terser/terser#minify-options
        terserOptions: {
          compress: {
            arrows: true, // 转换成箭头函数
            collapse_vars: false, // 可能有副作用,所以关掉
            comparisons: true, // 简化表达式,如:!(a <= b) → a > b
            computed_props: true, // 计算变量转换成常量,如:{["computed"]: 1} → {computed: 1}
            drop_console: true, // 去除 console.* 函数
            hoist_funs: false, // 函数提升声明
            hoist_props: false, // 常量对象属性转换成常量,如:var o={p:1, q:2}; f(o.p, o.q) → f(1, 2);
            hoist_vars: false, // var声明变量提升,关掉因为会增大输出体积
            inline: true, // 只有return语句的函数的调用变成inline调用,有以下几个级别:0(false),1,2,3(true)
            loops: true, // 优化do, while, for循环,当条件可以静态决定的时候
            negate_iife: false, // 当返回值被丢弃的时候,取消立即调用函数表达式。
            properties: false, // 用圆点操作符替换属性访问方式,如:foo["bar"] → foo.bar
            reduce_funcs: false, // 旧选项
            reduce_vars: true, // 变量赋值和使用时常量对象转常量
            switches: true, // 除去switch的重复分支和未使用部分
            toplevel: false, // 扔掉顶级作用域中未被使用的函数和变量
            typeofs: false, // 转换typeof foo == "undefined" 为 foo === void 0,主要用于兼容IE10之前的浏览器
            booleans: true, // 简化布尔表达式,如:!!a ? b : c → a ? b : c
            if_return: true, // 优化if/return 和 if/continue
            sequences: true, // 使用逗号运算符连接连续的简单语句,可以设置为正整数,以指定将生成的最大连续逗号序列数。默认200。
            unused: true, // 扔掉未被使用的函数和变量
            conditionals: true, // 优化if语句和条件表达式
            dead_code: true, // 扔掉未被使用的代码
            evaluate: true, // 尝试计算常量表达式
            // passes: 2, // compress的最大运行次数,默认是1,如果不在乎执行时间可以调高
          },
          mangle: {
            safari10: true,
          },
        },
      };
      return args;
    });
  }
  ...
}
复制代码
对于各参数,应该根据项目具体情况设定,加快运行速度或者减小代码体积的代价是增加了打包时间,如果不知道某个参数的含义最好不要修改。
复制代码

chunks总大小:15.8M => 15.7M(parsed)(主要提升运行时速度)

HtmlWebpackPlugin

HtmlWebpackPluginwebpack 的一个plugin,允许给每个入口指定一个html模板,来简化html文件的创建。vue.config.js的配置中的pages选项实际指定的就是HtmlWebpackPlugin的选项。

升级到了 V4.3,支持根据入口的chunk插入标签

升级版本的原因可以看前面踩坑中我写的博客

PreloadWebpackPlugin

这是一个HtmlWebpackPlugin的plugin,用于插入<link rel="prefetch"><link rel="preload">标签。由于PreloadWebpackPlugin V2.3不支持HtmlWebpackPlugin V4 ,这里升级到v3.0.0-beta.3(具体见issues)。 但是v3.0.0-beta.3 在多入口下会默认插入所有asyncChunks,必须用正则匹配,并且放弃了原本的按入口插入的选项。估计是认为prefetch不因被滥用,应当专门指定。

prefetch用来告诉浏览器在空闲时提前下载用户在接下来的浏览中可能用到的文件,preload则因为浏览器执行css时会阻塞DOM解析,所以通常用preload来提前下载当前页面马上要用到的文件

目前策略改为:prefetch入口相关的常用的asyncChunks,preload chunk-vendors和入口chunk

const HtmlWebpackPlugin = require('html-webpack-plugin');
const PreloadWebpackPlugin = require('preload-webpack-plugin');
...
chainWebpack: config => {
  Object.keys(config.entryPoints.entries()).forEach(page => {
    config.plugins.delete(`html-${page}`);
    config.plugins.delete(`preload-${page}`);
    config.plugins.delete(`prefetch-${page}`);
    /**
     * vue-cli内置的 v3.2 HtmlWebpackPlugin,插入的chunks必须显示指定
     * vue-cli默认指定的chunks是['chunk-vendors', 'chunk-common', page]
     * 但是修改了分片规则以后,完成分片之前不知道有哪些chunks
     * 这里全部替换为v4.3的HtmlWebpackPlugin
     */
    config
      .plugin(`html-${page}`)
      .use(HtmlWebpackPlugin, [{
        filename: `${page}.html`,
        // v4.3中,名为chunks,实为entries
        chunks: [page],
        template: templates[page],
      }]);
    /**
     * PreloadWebpackPlugin v2.3.0 不支持HtmlWebpackPlugin V4 ,
     * 这里替换为 v3.0.0-beta.3
     * @see https://github.com/GoogleChromeLabs/preload-webpack-plugin/issues/79
     * v3.0.0-beta.3 在多入口下会默认插入所有asyncChunks,必须用正则匹配
     * @see https://github.com/GoogleChromeLabs/preload-webpack-plugin/issues/96
     * 有人提了按入口插入的 pull request,将来也许可以不用这么麻烦
     * @see https://github.com/GoogleChromeLabs/preload-webpack-plugin/pull/109
     */
    config
      .plugin(`prefetch-${page}`)
      .use(PreloadWebpackPlugin, [{
        rel: 'prefetch',
        include: 'asyncChunks',
        fileWhitelist: [
          // your RegExp here
        ],
        fileBlacklist: [
          /\.map$/,
          /hot-update\.js$/,
        ],
        // v3.0.0-beta.3 没有includeHtmlNames选项了,只能通过excludeHtmlNames控制
        excludeHtmlNames: Object.keys(config.entryPoints.entries())
          .filter(entry => entry !== page)
          .map(entry => `${entry}.html`),
      }]);
    config
      .plugin(`preload-${page}`)
      .use(PreloadWebpackPlugin, [{
        rel: 'preload',
        include: ['chunk-vendors', page],
        fileBlacklist: [
          /\.map$/,
          /hot-update\.js$/,
        ],
        excludeHtmlNames: Object.keys(config.entryPoints.entries())
          .filter(entry => entry !== page)
          .map(entry => `${entry}.html`),
      }]);
  });
  ...
}
复制代码
要注意的是, preload 要放在 prefetch 之前。另外 prefetch 不应该被滥用,否则会造成占用大量服务器带宽。
复制代码

如果是在html模板中添加的脚本,PreloadWebpackPlugin是不会自动添加preloadprefetch的,需要自己手动添加

CorsPlugin

这是一个vue-cli内置的WebpackPlugin,用于给HtmlWebpackPlugin插入的tags加上crossorigin属性(脚本跨域)。因为我们的项目的静态资源(包括js和css)最终会放到CDN上,因此和项目的域名不同源,如果没有crossorigin属性,js报错是Script error,前端监控收集不到js报错的位置。如果<script>crossorigin属性,那么prefetchpreload也必须有crossorigin属性,否则prefetchpreload的资源是不会被使用的,会造成2次下载。CORS settings attributes

然而CorsPlugin不支持HtmlWebpackPlugin V4,因此需要重写一下CorsPlugin,在PreloadWebpackPlugin执行完后,通过正则表达式替换html里的prefetchpreload标签。

图片压缩

vue.config.js中添加一个图片压缩的loader:image-webpack-loader

npm install image-webpack-loader --save-dev
复制代码
chainWebpack: config => {
  config.module
    .rule('images')
      .use('image-webpack-loader')
        .loader('image-webpack-loader')
        .options({
          bypassOnDebug: true, // webpack 'debug' 模式下不执行
        })
        .end()
    .end()
}
复制代码

压缩效果:/dist 大小:91M=>83.1M,chunks总大小:16.3M=> 15.7M(parsed)

调整文件加载顺序

HtmlWebpackPlugin会把入口chunk插入到<body>末尾,因此写在html模板里的脚本会在入口chunk之前加载,它们会阻塞DOM解析,直到加载完成之后才开始加载入口chunk。所以,应该给不必要优先加载的脚本加上defer属性来延后加载,以及给需要优先加载的脚本加上preload(在所有css文件之前)。

defer:这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。

defer示意 (图片来源于网络,具体出处不明)

页面中文件的加载顺序容易被忽略,应当合理调整使得下载和执行能充分并行,不要让浏览器“闲下来”,这能很大程度上提高FMP
复制代码

HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,基本不用再担心文件数量过多。可惜项目的CDN不支持HTTP/2。

加快webpack打包速度

由于项目庞大,之前打包时间就很长,添加了image-webpack-loader之后,每次打包部署的时间增加了约1min,已经不太能忍受了。项目是在docker容器中拉代码打包部署的。检查了一下,打包时间的大头都在npm install和各种loader的执行上,如果不用每次打包都从0开始执行的话会快很多。首先可以在image-webpack-loader之前加上cache-loader,这样这个loader执行过一次后的数据会缓存在node_modules/.cache目录下,下次再打包就会利用缓存。

config.module
  .rule('images')
    // 给 image-webpack-loader 加上缓存来加快编译
    .use('cache-loader')
      .before('url-loader')
      .loader('cache-loader')
      .options({
        cacheDirectory: path.join(__dirname, 'node_modules/.cache/image-webpack-loader'),
      })
复制代码
注意:只有执行时间很长的loader才适合用缓存,因为读写文件也是有开销的,滥用反而会导致变慢
复制代码

在创建镜像时先打包一次得到node_modules目录。之后每次部署的时候,拉完代码后,将镜像中的node_modules直接移到代码目录下再执行npm installnpm run build。或者用npm install --cache-min Infinity利用npm缓存安装,再移动镜像中的.cache目录到代码目录下的node_modules里。而且能很大程度上避免因为网络不稳定导致的npm install下载文件慢的问题。

另外还有Freightnpmbox等离线包安装工具,可以根据项目情况使用

另外,如果生产环境不需要SourceMap,在vue.config.js中配置关掉。

productionSourceMap: false
复制代码

处理之后打包时间对比:

.cache目录没有.cache目录
原本部署时间190s309s
去掉 image-webpack-loader 之后159s332s
image-webpack-loader 加上 cache-loader 之后151s314s
去掉 SourceMap 之后102s257s

平时的代码写法建议

其实多数项目的性能问题有很多是平时写代码时一些不好的写法一点点引入的,你写的差一点,我写得差一点,时间长了就很难进行优化,因为涉及大范围的改动。这里给出一些写法的建议。

  • 新增的路由,考虑下要不要懒加载,看下有没有已有的chunk是同样的文件,要不要并入已有的 chunk,要不要 prefetch。
  • tabs 考虑下要不要懒加载(切换到对应tab时再渲染)
  • 比较大的依赖最好按需加载(加loading动画)
  • 项目内的图片的分辨率应该考虑使用场景的尺寸
  • 用户上传的图片可以考虑下要不要压缩
  • 新开选项卡会导致大量 js 再执行一遍,尽量思考一下是否有必要
  • 依赖库最好不要频繁变更,变更尽量集中在一起统一上线

优化效果

分片结果

分片结果

chunks总大小:16.09 MB(parsed)

各入口大小:2.3-4.8M(-60%)(parsed)

主要指标数据

经过前面的一顿操作,来看下产生了多少效果吧。

avg50分位90分位
TTI-11%-11%-11%
FCP-20%-18%-23%
FMP-11%-11%-14%
DOM Ready-15%-16%-14%

另外慢开比降低了67%,首次加载跳出率降低了47%。

可以看到TTI下降没有达到之前设定的目标,一方面是加载的资源太多了,有一些可以改成按需加载,另一方面是有些ajax请求比较慢,这个不在前端可控范围内了。后续会想其它办法类解决(可能考虑用service worker)。

未完待续(持续更新中)

文章分类
前端
文章标签