如何让首屏加载快584%,打包后资源体积缩小131%

482 阅读5分钟

概要内容

  • 项目背景
  • 性能优化结果
    • 打包后资源体积
    • 首屏页面加载速度
  • 优化步骤:
    • 压缩:Image\CSS\JS\Fonts\Html
    • Tree Shaking
    • splitChunks
    • dns-fetch
    • cdn加速

经过一周多的努力,我把项目打包后的资源减小了131%,首页加载速度提升了584%,下面直接给出对比数据方便查看,随后我会把我的优化方法分享给大家,希望对后门的同学提供一些帮助。

项目背景:

App 是由unity + 内嵌h5 的方式实现的,App 支持两种内核IE 和 Chrome。为了方便数据统计,Chrome 内核模式我直接利用Chrome测试的,直接拿的Chrome Network 面板的Finish 与 DOMContentLoaded 的数据。IE内核模式对于H5项目的早期版本,IE 会时不时的崩溃,所以只能通过代码的方式进行统计。

性能优化结果

打包后资源体积

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/30500bc0432448958efc0607f71b0ace~tplv-k3u1fbpfcp-zoom-1.image

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/259ae75ba2f74922bc97c263d32c6c5b~tplv-k3u1fbpfcp-zoom-1.image

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fcff6af3b55c4246b4b0f446742e54cd~tplv-k3u1fbpfcp-zoom-1.image

首屏页面加载速度

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fecc9dc2c0774db8b887afd4a61d4292~tplv-k3u1fbpfcp-zoom-1.image

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f87725f29f6641d992f6da40d04642c3~tplv-k3u1fbpfcp-zoom-1.image

图片处理

由于webpack对图片处理的压缩比不高,所以就利用陪伴我工作很多年的图片压缩工具**tinypng**来进行压缩

CSS处理

抽离CSS文件,filename 命名格式采用contenthash 利用cdn

chunkhash存在一个问题,就是js关联的css,只要js变化,打包后的cs的hash值也会变化,所以我们改成contenthash

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/553c1f4f078b4056ab0df27a99664385~tplv-k3u1fbpfcp-zoom-1.image

压缩CSS:css-minimizer-webpack-plugin

optimization: {
  minimizer: [new CssMinimizerPlugin()],
}

JS处理

Tree Shaking

  • 测试代码

    //src\utils\math.js
    export function square(x) {
      return x * x;
    }
    
    export function cube(x) {
      return x * x * x;
    }
    
    //src\main.js
    import { cube } from "@/utils/math";
    
    console.log("cube:", cube(5));
    
  • 修改配置

    • mode 设置成development (原因:为了方便查看打包后的结果)
    • 添加usedExports

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/65fa30464891432590cab05e7e66b186~tplv-k3u1fbpfcp-zoom-1.image

    • 打包后结果:未被使用的代码,已标记

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a8fef582ff146beac9df321fd72f98b~tplv-k3u1fbpfcp-zoom-1.image

    • 通过设置mode为production,默认已支持了tree shaking,所以不用再配置了

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3b679c4b21349d8a72b515e1bd3e230~tplv-k3u1fbpfcp-zoom-1.image

    查看打包后的结果

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b35574e4064b451996743b104ed88174~tplv-k3u1fbpfcp-zoom-1.image

压缩:terser-webpack-plugin

optimization: {
      minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
},

Fonts

  1. 删除废弃的资源
  2. 去掉重复的图标

Html

只需要将mode 改成production即可,webpack5 内置插件对html进行了压缩,所以不需要做什么

分析打包后的资源

在进行cdn加速之前,我们需要安装webpack-bundle-analyzer,来直观的分享我们打出来的包到底什么在占用我们的资源体积

{
	plugins:
	[
		...
		new BundleAnalyzerPlugin()
	]
}

再次打包后自动启动一个本地服务,内容如下:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b72f7e74969147b4b57880b500840f74~tplv-k3u1fbpfcp-zoom-1.image

根据此图:我们就可以快速定位到哪些模块在占用体积,所以我们把大的模块缩小(比如:删除冗余或废弃、按需引入、寻找更小的库替代等)。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/38c3a8f4598b4759b2a33467da9b5f8c~tplv-k3u1fbpfcp-zoom-1.image

把element-ui\vue\vue-router\vuex等改成cdn形式,再次打包看bundle analyzer,发现这些模块都没有显示了。(提示:这个图我们还可以继续优化)

接下来把抽离的库,打包过程中自动注入到模板html文件中,为此我单独写了一个插件 html-dynamic-injection (提示:后面我会将此插件写成单独的一个库,然后发布至npm)

CDN

根据html-dynamic-injection添加配置,自动注入cdn配置、style配置、dns-fetch配置,格式如下

  • 参数规则

    {
      scripts: [
        {
          importName: "",
          globalVariableName: "",
          position: "head" | "body",
          src: "",
          integrity: null | "",
          async: false | true,
          defer: null | "defer",
          rel: null | "preload" | "prefetch",
          importance: "high" | "low" | "auto",
          crossorigin: null | "anonymous",
        },
      ],
      links: [
        {
          importName: null | "",
          href: "",
          ref: null | "stylesheet" | "dns-prefetch",
          crossorigin: null | "anonymous",
        },
      ],
    }
    
  • 添加配置(提示:为了展示以下只是部分配置)

    function getExternalLibs(isMin) {
      let extension = isMin ? ".min.js" : ".js";
      let cssExtension = isMin ? ".min.css" : ".css";
      return {
        scripts: [
          ...
          {
            importName: "element-ui",
            globalVariableName: "Element",
            position: "body",
            src: `https://unpkg.com/element-ui@2.15.6/lib/index.js`,
            defer: "defer",
            async: true,
            importance: "low",
            crossorigin: null,
          },
        ],
        links: [
           {
            importName: "element-ui/lib/theme-chalk/index.css",
            ref: "stylesheet",
            href: `https://unpkg.com/element-ui@2.15.6/lib/theme-chalk/index.css`,
            crossorigin: null,
          },
          {
            rel: "dns-prefetch",
            href: "https://unpkg.com",
            crossorigin: null,
          },
        ],
      };
    }
    
  • 引入插件 + 使用

    const HtmlDynamicInjectionPlugin = require("../plugins/html-dynamic-injection");
    ...
    
    plugins: [
      ...
      new HtmlDynamicInjectionPlugin(getExternalLibs()),
    ]
    
  • 打包后结果

    <!doctype html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8" />
        <title>title</title>
        <link rel="stylesheet"
              href="https://unpkg.com/element-ui@2.15.6/lib/theme-chalk/index.css">
        <link rel="dns-prefetch"
              href="https://unpkg.com">
        <link rel="dns-prefetch"
              href="https://cdn.jsdelivr.net">
        <script crossorigin="anonymous"
                position="head"
                src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
        <script crossorigin="anonymous"
                position="head"
                src="https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js"></script>
        <script crossorigin="anonymous"
                position="head"
                src="https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js"></script>
        <script defer="defer"
                src="/static/js/main.ecae5beeb5463b7c5f8a.js"></script>
        <link href="/static/css/main.bfcb8559ae29d81822a8.css"
              rel="stylesheet">
    </head>
    
    <body>
        <div id="app"></div>
        <script position="body"
                src="https://unpkg.com/element-ui@2.15.6/lib/index.js"
                defer="defer"
                async
                importance="low"></script>
    </body>
    
    </html>
    

    补充说明:注入资源到html里面是有讲究的

    1. link配置到head:快速解析dns解析域名
    2. style配置到head:提前把样式加载,避免页面展示出来样式错乱
    3. script 引入模块,引入位置、defer、importance、async、pre-fetch、pre-load 都会影响加载顺序和速度,推荐还是直接去MDN Web docs仔细去了解这些参数,来应对未来的网站性能优化
      1. 比如:vue 需要最先加载,所以我们将其放到head中
      2. 比如:element-ui,不是首页立马显示的,所以我们放到body中

    具体demo:传送门

总结

  • 不要止步不前,要跟随行业走:如果项目有必要,找到合适的时机,就及时更新项目环境(比如:webpack5 已经内置很多插件,代码分割、混淆 等等,都已经有默认配置了,无特殊要求可以不用配置)
  • 如果对CDN 稳定性有要求,最好使用公司自己的,不要使用公共的CDN (原因:公共CDN 时快时慢,挺不稳定的)

遗留问题:

  • element-ui : 自动注入css到html 与 手动在html模板中引入css,引入后的内容完全一样,但结果却是手动引入的可以成功加载,自动注入的却不加载,未找到具体原理,如果你知道原因,麻烦告诉我,感谢

至此感谢您的阅读,如果您有什么想法,欢迎给我留言~

以上:如发现有问题,欢迎留言指出,我及时更正


参考文献