首屏加载速度优化的理解

前言

首屏性能能带来什么,认清它的意义,不盲目优化。

本文带来的内容仅仅是本人觉得可圈可点的常用优化手段,这些优化手段在网络上的文章也能随意看到,我会以一个周期顺序的维度去分别拿出具有代表的手段讲述,如有错误欢迎指正。

编译时

编译时指的是我们对项目工程化过程中能做的事

Webpack split chunk

module.exports = {
  //...optimization: {
    splitChunks: {
      chunks: 'async',
      // 内容超过了minSize的值,才会进行打包
      minSize: 20000,
      // 确保拆分后剩余的最小 chunk 体积超过此项的值,防止出现大小为0的模块
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      // 超过这个值就会进行强制分包处理,无视 minRemainingSize,maxAsyncRequests,maxInitialRequests
      enforceSizeThreshold: 50000,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
}

**Q:**为什么要这样分包,这样分包的好处是?
**A:**浏览器 HTTP 单域名最大并发请求数量是有限的。只要文件够散,体积够小,加载速度会更快。一般的 SPA 项目可能不太容易体验到这个提升,如果您在 3G 网络下去体验您就能感受到这个差距了。
**TIPS:**这里也引出了单域名是有限的,所以我们会看到很多网站它的 HTML 文件跟资源文件是非同域的,当然这里也有很大一部分原因是因为资源文件都在对象储存上了。一些比较讲究的网站会出现不一样的资源在不一样的域名,这也是一种提升手段。但反之带来的是更多域名的 TTFB 时间。

Webpack DllPlugin

DllPlugin 是什么,这边请您自己看文档

其实大概能看到 DllPlugin 的作用跟上面的 split 比较相反,它是把几个文件聚合成一个大文件,这样做的好处就是第二次访问直接 http from disk。
Q:是不是跑题了,这里要做的不是首屏加载吗,你这样是不是反优化了?
A:假定一个场景,比如常用的 @yourCompany/* 打成一个 dll,放在一个公共的对象存储服务上,独立的域名。当用户访问过你企业的 A 项目,以后再访问企业的 B 项目的首屏,就会直接 from disk。还可以把一些常用的请求库,运行时框架打包成 dll,一样的效果。 值得一提的是,有个这样的插件可以帮你自动配置 DLL:autodll-webpack-plugin

常用三方包体积优化

一些比较常用的三方包,其实可以单独对他们做一些体积优化。最常见的是 moment.js

module.exports = {
    ...
    plugins: [
        // 忽略moment的语言包打包
        new webpack.IgnorePlugin(/\.\/locale/, /moment/)
    ]
}

import 'moment/locale/zh-cn';

// 你是不是经常这样做?

其实你不需要这样做,有人写了插件哈哈哈哈哈哈哈哈。moment-locales-webpack-plugin
更重要的是,谷歌还总结了有哪些库可以做这些优化。

Core-js 的体积优化

目前比较常见的就是调整 .browserslistrc

其实也可以动态垫片

// 动态垫片使用示例
import HtmlTagsPlugin from "html-webpack-tags-plugin";
export default {
    plugins: [
        new HtmlTagsPlugin({
            append: false,
            publicPath: false,
            tags: ["https://polyfill.alicdn.com/polyfill.min.js"]
        })
    ]
};

其他

  1. 一些常用的插件,比如 terser-webpack-plugin,tinyimg-webpack-plugin...
  2. 升级 webpack5

Q: 升级 webpack5 有什么好处?
**A:**据我了解 webpack5 做的大多是编译速度提升,本来代码优化就留给 loader 和 plugin 生态, 但是内置的 Tree Shaking 有一点改变,变得更深度了。

import * as API from '@/api';

const { nickname, avatar } = await API.GET_USER_INFO();

/*
webpack4 => import * as API from './api';
webpack5 => import { GET_USER_INFO } from './api';
*/
  1. 慎用 img-loader 把图片转成 BASE64BASE64 这个编码决定了大小会是原图的 1.3 倍左右

加载时

TTFB

Time To First Byte 个人理解是 DNS 查找时间 + 三次 TCP 握手 + SSL/TLS 握手,以下这些属性有很多使用场景,比如预解析 image 资源的 cdn 域名,预连接登陆接口的域名

  • 上靠谱的 CDN,镜像节点只要足够多,距离足够近连接的时间也会快那么一丢丢
  • Link 标签的预连接,提前握手,preconnect
  • Link 标签的预解析,提前解析,dns-prefetch

资源加载优先级

您可以利用以下这些标签和属性来做一些资源文件的加载优先级处理

  • Link 标签的 importance,实验属性且存在兼容性和局限性问题
  • Link 标签的 preload,先加载有需要再执行
  • Script 标签的 defer,先加载后执行

资源加载速度

  • 与上面编译时提到的一样,资源分类拆分多域名,能增加请求并发数
  • GZIP,gzip_comp_level 等级谨慎调整,毕竟压缩也是 CPU 密集,不过现在几乎都是 对象存储 + CDN 傻瓜式了
  • 组件或页面按需加载,这个不赘述大家也知道怎么用,但是值得一提的是。我们经常能看到有人会把入口页面懒加载。这样其实是反优化,懒加载的意义在于你的这个资源并非现在必须立刻执行的。如果你把必须立刻执行的资源懒加载,这样等于把一个文件拆成了两个文件,而且还会增加一条 http 请求。

    懒加载不一定是 React.lazy 或者 Router 相关的操作,您可以自己在某些场景 await import('https://xxx.xxx.com/a.js')

  • 图片处理
    • 懒加载,这个甚至不值得拿出来说了
    • Webp,建议自己提前转好 webp 传到对象储存服务上,这样会省去很多对象储存把图片转成 webp 的时间。如果你不这样做,其实优化的效果并不是特别大
    • 不支持 webp 的情况下其实如果是移动端,您也可以考虑只取手机屏幕宽度 1.5 倍大小的图片,为什么是 1.5 倍,因为考虑苹果高清屏
    • 精灵图,什么场景更合适呢?UI 预制的小图标(如默认表情包)
    • icon 建议用字体,如果只有一两个,建议用 SVG 字体库库只摘相应的字体
<!-- 上面 webp 和 jpg 的兼容为伪代码 -->
<picture class="picture">
  <source type="image/webp" srcset="https://test.abc/example.webp">
  <img class="image" src="https://test.abc/example.jpg?x-oss-process=image/resize,l_{window.screen.width * 1.5}">
</picture>
  • 骨架屏,骨架屏为什么属于加载时?因为我对它的理解是用来过度“加载完成 html 文件但未执行 js 代码前”这段时间它应当存在,所以你只要直接做个 SVG 在 html 文件里面就 ok 了,且按着渲染规则让它最早渲染就完事了。往往很多时候人们都喜欢把它写在运行时里面,实际上这算是一种反优化,“加载完成 html 文件但未执行 js 代码前”这个时间节点是白屏的且你多了一次重绘,且浪费时间显示骨架。如果页面已经可以去请求接口了,这个时候显示的不应该是骨架,而是页面的 dom & style

运行时

选型

您可以考虑选择一些弱运行时的框架

冗余代码

  • 环境变量常见错误使用场景
    // fail
    const API_DOMAIN = process.env.NODE_ENV === 'prod' ? 'www.xxx.com' : 'www.dev.xxx.com';
    fetch(API_DOMAIN);
    
    // recommend
    // .env
    API_DOMAIN = www.xxx.com
    
    // .prod.env
    API_DOMAIN = www.dev.xxx.com
    
    fetch(process.env.API_DOMAIN);
    
  • 不要阻塞渲染,比如接口请求返回再去渲染整个 dom,您应该考虑 dom 先渲染,对应 API 信息的内容返回后渲染

CSS

减少重绘和回流

  • 其实 vdom 几乎帮我们做完了
  • 使用 class 操作样式,而不是频繁操作 style
  • 避免 table 布局
  • 批量操作 DOM,createDocumentFragment / react
  • 节流防抖 resize
  • transform
  • ......

数据

您应该有一些监控数据来体现你的优化是否带来实际作用,也应该从数据中找到还可优化的点。您也应该做一些归因分析去说服产品做一些能在体验和技术速度之间折中的方案来提供首屏,比如游客模式。 同时你也可以用 lighthouse 或者 webpagetest 之类的工具去观察您的页面的 TTFB,FCP,LCP,TBT 这些值

© 2024