4 min read
前端性能优化实践记录

在实践后发现每一个维度,每一种优化方案下都有非常多的技术细节,如果要全面极致的做好优化,不仅是从全局到细节、从技术储备的广度到深度的考验,很多的优化手段或者方向,如果没有实践可能完全会是盲区。狭义的讲优化无非是用更少的时间做更多的事,从各种角度减少不必要的资源(网络、渲染、I/O 等)开销。更宽泛的讲,我认为其实上到页面合理直接的交互设计,操作逻辑的去繁从简也可以包括在优化之中,这么说好像其实是整个系统工程的整体优化,最终体现到网页的加载速度上。

性能优化指标

FCP

First Contentful Paint 标记了绘制出首个文本或首张图片的时间

FCP 衡量的是用户转到您的网页后,浏览器渲染第一段 DOM 内容所用的时间。 您网页上的图片、非白色 <canvas> 元素及 SVG 都被视为 DOM 内容;iframe 中的任何内容均不会包含在其中

alt text

FCP 优化方案

移除阻塞渲染的资源

视为阻塞渲染的资源

<script>

  • 在文档的 <head>
  • 没有 defer 属性
  • 没有 async 属性

<link rel="stylesheet">

  • 没有 disabled 属性
  • 没有 media 属性
  • media="all" 属性
缩减 CSS 大小
移除未使用的 CSS
移除未使用的 JavaScript
预先连接到所需的源
缩短服务器响应时间 (TTFB)
避免多次网页重定向
预加载密钥请求
避免网络负载庞大
采用高效的缓存政策提供静态资源
避免 DOM 规模过大
最大限度地缩短关键请求深度
确保文本在网页字体加载期间保持可见状态
尽量减少请求数量,减少传输大小

计算 FCP

使用 JavaScript 获取 FCP 时间

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntriesByName("first-contentful-paint")) {
    console.log("FCP candidate:", entry.startTime, entry);
  }
}).observe({ type: "paint", buffered: true });

API 与指标计算方式之间的差异

  1. API 会为后台标签页加载的网页分配first-contentful-paint,计算 FCP 时应该忽略
  2. 从 往返缓存 中恢复网页时,API 不会报告 first-contentful-paint 条目,计算 FCP 时应该要考虑到这种访问情况
  3. API 可能不会报告跨源 iframe 的绘制时间
  4. API 从导航启动时开始测量 FCP,但对于预渲染的网页,应通过 activationStart 测量 FCP

实际使用时还是用现成的包检查计算 FCP 的差异吧~

web-vitals

import { onFCP } from "web-vitals";

// Measure and log FCP as soon as it's available.
onFCP(console.log);

优化方案细节

资源压缩-[源码]

gzip 压缩

vite 开启 gzip 压缩

vite-plugin-compression gzip 压缩

@rollup/plugin-terser Terser 优化

webpack 等打包工具等大同小异,忽略

nginx 配置 gzip_static

  • nginx 动态压缩 (对请求先压缩再输出)

  • nginx 静态压缩 (直接输出.gz 的预压缩文件)

nginx.conf

http {
  # 开启gzip模式
  gzip on;
  # 是否传输gzip压缩标志,在header中添加 Vary: Accept-Encoding
  gzip_vary on;
  gzip_static on;
  # 压缩文件类型
  gzip_types text/css application/json application/javascript application/x-javascript text/javascript font/truetype font/opentype image/webp image/svg+xml;truetype font/opentype image/webp image/svg+xml;
}

动态加载模块

const Home = lazy(() => import("./pages/Home"));

优点

  1. 按需加载,减少首屏加载时间
  2. 懒加载
  3. 代码拆分,减少体积

缺点

  1. 兼容性,现代浏览器和 node 环境原生支持
  2. 需要注意加载错误的情况,看情况是否需要 catch 捕获
  3. 切换页面资源时也需要重新加载,会出现闪烁的现象

资源压缩-[媒体文件|图片]

使用 cwebp 将图片转换为 WebP 格式

支持PNGJPEGTIFFWebP转换成WebP格式

安装压缩 webp 格式工具

  brew install webp

使用

cwebp -q 80 -m 6 -noalpha ./public/img/bg/cover-high.jpeg -o ./public/img/bg/cover-high.webp

参数

参数名参数值说明
-q0-100RGB 通道压缩系数
-m0-60 最小压缩 6 最大压缩
-noalpha无 alpha 通道
-ostring指定 WebP 输出文件的名称
-lossless无损压缩

刚开始压缩我的 jpeg 反而变大了,还是需要调整参数哈哈,而且需要考虑浏览器兼容性

Google 关于图片格式的选择指南

alt text

格式优点缺点适用场景
gif文件小,支持动画、透明,无兼容性问题只支持 256 种颜色色彩简单的 logo、icon、动图
jpg色彩丰富,文件小有损压缩,反复保存图片质量下降明显色彩丰富的图片/渐变图像
png无损压缩,支持透明,简单图片尺寸小不支持动画,色彩丰富的图片尺寸大logo/icon/透明图
webp文件小,支持有损和无损压缩,支持动画、透明浏览器兼容性不好支持 webp 格式的 app 和 webview

加载时机优化

script 优化

defer

异步加载/HTML 解析 => HTML 解析完成 => 执行

<script defer type="module" src="/src/main.tsx"></script>

编译后

<head>
  <script type="module" crossorigin src="/assets/index-8RLgu2UJ.js"></script>
</head>

async

异步加载/HTML 解析 => 加载完成 => HTML 解析完成 => 执行

我有一个需要注入到 window 变量的 js 脚本,使用的 async 异步加载

<script async type="text/javascript" src="/ScrollSmoother.js"></script>

HTTP2

二进制分帧层

HTTP/2 所有性能增强的核心是新的二进制分帧层,它规定了 HTTP 消息的封装方式,并在客户端和服务器之间传输。

二进制分帧层

多路复用

多路复用

数据流优先级

数据流优先级

每个源一个连接

流控制

服务器推送

服务器推送

标头压缩

霍夫曼编码(HPACK 压缩)对标头进行压缩,并要求服务端和客户端维护一份标头索引以建立压缩上下文

标头压缩

nginx 开启 HTTP2

http {
  server {
    listen 443 ssl http2;
  }
}

缓存策略

往返缓存

nginx 开启

注意:往返缓存和委托行为无法同时开启

http {
  # 缓存路径
  proxy_cache_path /assets levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
  server {
    location / {
      proxy_cache my_cache;
      # 设置缓存有效时间为7天
      proxy_cache_valid 200 302 7d;
      # 缓存 404 响应 1 分钟
      proxy_cache_valid 404 1m;
      # 设置往返缓存
      proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
      # 允许 Nginx 在后台异步更新过期的缓存
      proxy_cache_background_update on;
      # 确保只有一个请求能够更新缓存,防止缓存过期时多个请求同时更新缓存,造成服务器负载过高
      proxy_cache_lock on;
      # 是否命中缓存
      add_header X-Cached $upstream_cache_status;
    }
  }
}

使用 CDN 服务

目前只用了 react 等跨多页面共用资源的 CDN,自己的静态资源还没有使用 CDN 服务,目前工程体量还没有很大,delay 一下哈哈。

更多实践真实用上之后再更新…