1-6 性能优化

342 阅读11分钟

原文链接(格式更好):《1-6 JS 性能优化》

指标

Performance Timing API

浏览器的性能指标可以通过 Performance Timing API 来获取,它是一组 API 的集合,常用的有:

  1. Navigation Timing API:包含了从页面导航开始到页面加载完毕的一系列耗时 API(PerformanceNavigationTiming),通过window.performance.getEntries()[0]/performance.getEntriesByType('navigation')[0]获取
  2. Resource Timing API:包含网页资源(脚本、样式、图片等)加载的耗时 API(PerformanceResourceTiming),通过window.performance.getEntries()[*]/performance.getEntriesByType('resource')[*]获取
  3. Paint Timing API:包含网页绘制相关的耗时 API(PerformancePaintTiming),通过window.performance.getEntries()[*]/performance.getEntriesByType('paint')[*]

其中window.performance.getEntries()获取到的是个数组,里面包含一系列 API,并且同时存在多种 API,以下是浏览器打印的结果

performance.getEntriesByType('type') 可根据类型获取到对应 API 数组

Navigation Timing API

官方文档:www.w3.org/TR/navigati…

我们这主要分析Navigation Timing API,记录页面导航到页面 load 加载完毕的一系列事件,其中跟Resource Timing API也有关联

通过window.performance.getEntries()[0]获取,浏览器打印结果如下:

  • name:地址栏的值
  • entryType:值为navigation,表明是一个PerformanceNavigationTiming实例
  • startTime:开始时间,毫秒
  • duration:总耗时,毫秒,从导航开始到 load 加载完的时间,等价于loadEventEnd - startTime

Navigation Timing Level 2

包含的属性很多,但它们之间是存在联系的,图片来自:Navigation Timing Level 2

流程解读

1. startTime

开始时间,一般为0

2. Process Unload Event

进程解锁事件,一般是执行上一个页面的unload事件(若有),记录两个时间:unloadEventStart/unloadEventEnd

3. Redirect

重定向事件,若有重定向则记录两个时间:redirectStart/redirectEnd

同域名下时,可以直接用该值;若不是同域名,该值不太准确

4. Service Worker Init

初始化service worker,若有启动,则记录启动时间:workerStart

5. Service Worker Fetch Init

初始化service worker 的 fetch,若有启动,则记录启动时间:fetchStart

一般为浏览器开始获取 HTML 的时间

6. HTTP Cache

从缓存里面的找数据,这一步无计时点,但可以通过:domainLookupStart - fetchStart来计算缓存找数据的耗时

7. DNS

开始进行域名的查找,记录两个时间:domianLookupStart/domainLookupEnd

8. TCP

TCP 连接,记录两个时间:connectStart/connectEnd

若是 HTTPS 协议,则额外有安全连接的开始时间: secureConnectStart

connectStart 与 domianLookupStart 之间的差值为:类型判断的耗时,因为需要判断是 HTTP/HTTPS、短链接/长链接 等等

9. Request

发起请求,记录时间:requestStart

10. Early Hints

早期提示,跟 HTTP 的状态码103挂钩,一般告知浏览器一些子资源(JS/CSS等),便于提前加载,可以记录的时间有:interimResponseStart

11. Response

返回响应,记录时间:responseStart/responseEnd

12. Processing

参考:Document: readyState property - Web APIs | MDNDocument: DOMContentLoaded event - Web APIs | MDN

处理,一般指的是HTML、CSS、JS 等资源的加载与解析,记录的时间有:

domInteractive:HTML 加载、解析完成(DOM 树解析完成),但其他资源可能还在加载

domContentLoadedEventStart/domContentLoadedEventStart:HTML 加载、解析完成,并且所有延迟 JS(<script defer src="…"> 和<script type="module">) 已下载并执行时触发

domComplete:HTML与所有子资源加载完(Render 树解析完成)

13. Load

参考:Window: load event - Web APIs | MDN

HTML与所有子资源加载完后触发,记录两个时间:loadEventStart/loadEventEnd

实际运用

指标解读:

  • Total:总时间,各项指标之合
  • DNS:domainLookupEnd - domainLookupStart,DNS查询花费的时间
  • TCP:connectEnd - connectStart,TCP 建立连接花费的时间
  • Request:responseStart - requestStart,请求到响应花费的时间
  • Response:responseEnd - responseStart,接受响应花费的时间
  • Processing:domComplete - domInteractive,渲染页面花费的时间
  • Load:loadEventEnd - loadEventStart,load 阶段花费的时间

CWV:Core Web Vitals - 核心 Web 指标(谷歌)

谷歌提出的,从:加载、交互、视觉稳定性三个方面衡量

加载:LCP - Largest Contentful Paint

LCP(最大内容渲染) 应该在页面首次加载后 2.5s 内发生,白话:在前 2.5s 内完成最大内容的渲染

LCP 的定义为:

  1. 图像元素的加载: 当 / 元素加载并成功渲染到屏幕上。
  2. 背景图像: 如果是通过 CSS 的 background-image 设置的背景图像,当该背景图像被渲染到屏幕上。
  3. 文本元素: 包含大块内嵌内容的块级元素

LCP 值低的原因:

  1. 资源问题:
    1. 加载慢:图片/背景等都存在依赖外部资源的情况,所以可以先在domContentLoadedEvent内进行埋点,看看是否是服务器资源响应慢导致的
    2. 下载慢:资源很快返回了,但可能由于资源太大/链路太长,导致下载慢
  1. 渲染问题:
    1. 渲染被阻断了:一般是 CSS、JavaScript 阻断的
    2. 单纯渲染慢:可能是客户端硬件的影响

针对性改造:

  1. 服务器优化
    1. 缓存 HTML 离线页面:将一样的部分/资源进行离线缓存,这样就不需要再通过服务器获取了
    2. 对图片的优化
      1. 不同场景使用不同格式的图片,降低图片大小,加快请求速度
        1. JPEG:有损压缩,网站中的摄影图片、细节丰富的图片等
        2. PNG:无损压缩,网站图标、LOGO等
        3. WebP:有损/无损压缩,在支持 WebP 的浏览器中使用
        4. SVG:矢量图标,图标、LOGO 等
      1. 云资源管理
    1. 减少文件大小
      1. 去重、压缩、过滤等操作
        1. Webpack、Vite 等工具可提供
      1. CDN - 内容分发网络(Content Delivery Network) 
        1. 物理上接近请求点,减少延迟,提高加载速度
  1. 客户端优化
    1. 渲染阻断的优化
      1. CSS、JS 进行延迟处理
        1. 初次渲染做很多事情并不是很好的,所以可以先用“骨架屏”完成初次渲染,再去写请求数据之类的逻辑,最后填充数据,这样的 LCP 值更低
      1. 首屏优化(单页应用)
        1. 懒加载
          1. 页面模块、组织模块等
        1. 异步加载
          1. 组件本身、样式本身等
      1. CSS 模块化
      2. SSR服务端渲染

交互:FID - First Input Delay

FID 的定义:

FID(首次输入延迟):用于衡量用户首次交互到浏览器能够响应的时间

指标:页面的 FID 应该小于 100ms

FID 值低的原因:

  • 执行的阻塞

针对性改造:

  • 减少 JS 的执行时间,因为 JS 是单线程,执行时会阻塞,大于 50ms 的被称为长任务
    • 压缩 JS 文件,可以过滤掉多余打印,提升执行效率
    • 延迟加载不需要的 JS
      • 模块懒加载
        • 有些模块在首屏不需要展示时,一开始可以不用去加载
      • tree shaking
        • 用于消除 JavaScript 中未引用代码(dead code)的术语。这个过程类似于摇动一棵树,抖落树上的枯叶,只留下需要的部分。
        • 最常见的是引入了 xx 库,但只使用了该库一些功能,则打包的时候也应该只打包已使用的功能
    • 减少未使用的 polyfill(拦截)
      • 通常是为了兼容低版本的浏览器,而所做的弥补性代码
      • 比如:xx 版本浏览器不支持 includes,则可以这样 polyfill
// Polyfill for Array.prototype.includes
if(!Array.prototype.includes){
  Array.prototype.includes = function(element) {
    return this.indexof(element) !== -1
  }
}
      • 建议提前做好浏览器版本判断,高版本的话就不走 polyfill 代码逻辑了
  • 分解耗时任务
    • 减少执行长的逻辑代码
      • 比如双重数组循环,分成两个循环(虽然性能会差),但阻塞更小
      • 比如:表格-先请求列然后再请求数据
        • 若是请求嵌套:先列请求然后在里面请求数据,最后再赋值渲染表格
        • 若是请求串行:先列请求-赋值渲染空表格,加 loading,再请求数据-赋值渲染表格数据,这样的阻塞更小
    • Worker
      • 采用 Worker,去分场景承担耗时任务

视觉稳定性:CLS - Cumulative Layout Shift

CLS 的定义:

累积布局偏移,衡量页面上元素位置发生变化的频率与程度

指标:页面的 CLS 应该小于 100ms

简单来说就是页面渲染时,元素的位置是否“稳定”

CLS 值低的原因:

  • 无尺寸的图片、视频、iframe 等
  • 动态内容插入
  • 字体的突然改变

针对性改造:

  • 不使用无尺寸元素
    • 图片可以使用:srcset & sizes
      • srcset:描述图片资源与其像素宽度
      • sizes:设置图片在不同屏幕下要展示的宽度,默认为 100vw
<img 
  src="1.png" 
  srcset="1.png 200w,2.png 400w,3.png 800w" 
  sizes="(max-width: 300px) 200px,
  			 (max-width: 700px) 400px,
  			 (max-width: 1000px) 800px,
         100vw
  			"
/>
<!-- srcset 设置后为:(0,200px],(200px,400px],(400px,800px],(800px,∞], -->
<!-- 当屏幕最大宽度为 300px 时,sizes 使用 200px,对应 srcset 里面的 1.png-->
<!-- 当屏幕最大宽度为 700px 时,sizes 使用 400px,对应 srcset 里面的 2.png-->
<!-- 当屏幕最大宽度为 1000px 时,sizes 使用 800px,对应 srcset 里面的 3.png-->
<!-- 当屏幕最大宽度大于 1000px 时,sizes 使用 100vw(根据屏幕实际宽度),最终对应 srcset 里面的还是 3.png-->
  • 整体化内容插入
    • 相对集中的去完成内容的插入
  • 减少动态字体插入

CWV 谷歌浏览器插件:Core Web Vitals Annotations

性能评估- performance

前端性能优化 — 保姆级 Performance 工具使用指南 - 掘金

大厂监控体系

  1. 建立
    1. 埋点上报
      1. 获取关键节点的时间
      2. 点对点
      3. 信息采集
    1. 数据处理
      1. 数据分类
        1. 请求类、渲染类、交互类等等
      1. 阈值设置
      2. 数据重组()分组
        1. 多维度组装数据,分析对应结果数据的
    1. 可视化展示
      1. 自研
      2. 开源:grafana(Grafana 中文入门教程)...
  1. 评估
    1. 根据数据指标进行数据圈层()数据归档
    2. 定位问题
  1. 修复
    1. 告警通知
    2. 分派处理

补充知识

Web Worker

定义

基于浏览器的独立线程

特点

  1. 独立性:与主线程独立,有自己的全局作用域与执行环境
  2. 无法访问 DOM:没法访问主线程的 DOM,适合做纯计算或数据处理
  3. 通信:可以与主线程通信

基本使用

注册 Web Worker(A.js)

const worker = new Worker('./web-worker.js')

// 发送消息
worker.postMessage({ data: 'hello web worker'})

定义 Web Worker(web-worker.js)

// 接收消息
self.onmessage = function(event) {
  console.log('Message from A.js:', event.data);
}

实际场景之斐波那契数列计算

注册 Web Worker(A.js)

  const webWorker = new Worker("./worker/web-worker.js");
  webWorker.postMessage({ type: "calculateFibonacci", n: 10 });

  webWorker.onmessage = (event) => {
    const { type, result } = event.data;

    if (type === "calculateFibonacci") {
      const calculateFibonacciResult = result;

      // do something...

      //   结束 Worker
      webWorker.terminate();
    }
  };

定义 Web Worker(web-worker.js)

self.onmessage = function (event) {
  const { type, n } = event.data;

  if (type === "calculateFibonacci") {
    const result = calculateFibonacci(n);

    self.postMessage({ type: "calculateFibonacci", result });
  }
};

function calculateFibonacci(n) {
  if (n <= 1) return n;
  else {
    return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
  }
}

Service Worker

定义

基于浏览器,但独立于网页的脚本运行容器。

特点

  1. 独立性:独立于网页的脚本运行,网页关闭也可运行的。
  2. 网络代理:可以拦截和处理浏览器的网络请求
  3. 事件驱动:可以监听浏览器的各种事件

常用于:消息推送通知、离线缓存、拦截处理网络请求

基本使用

注册 Service Worker (A.js)

if('serviceWorker' in navigator){
  navigator.serviceWorker.register('./..../service-worker.js')
  .then(registration => {
    console.log('注册 serviceWorker 成功', registration)
  })
  .catch(error => {
    console.log('注册 serviceWorker 失败', error)
  })
}

定义 Service Worker(service-worker.js)

// 监听安装事件,一般用用于设置浏览器的离线缓存
self.addEventListener('install', event => {
  // do something ....  
})

// 监听 fetch 事件,一般用于拦截和处理请求
self.addEventLinstener('fetch', event => {
  // do something ....
})

实际场景之离线缓存

注册 Service Worker

注册代码和基本使用的一致,不累赘

定义 Service Worker

const CACHE_NAME = 'myCache1'
const CACHE_URL = ['/script/main.js', '/script/xxx.js', '/style/xxx.css']

// 监听 Service Worker 的“安装”事件:创建新缓存
self.addEventListener('install', event => {
  // waitUntil表示在异步操作之前不要终止
  event.waitUntil(
    // 创建名为 CACHE_NAME 的缓存版本
    caches.open(CACHE_NAME).then(cache => {
      // 指定要缓存的地址内容
      return cache.addAll(CACHE_URL)
    })
  )
})

// 监听 Service Worker 的“激活”事件:清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if(cacheName !== CACHE_NAME) {
           return caches.delete(cacheName)
          }
        })
      )
    })
  )
})

// 监听 Service Worker 的“请求”事件:更新缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request)
    })
  )
})

FCP - First Contentful Paint

首次内容渲染,衡量用户首次看到内容的时间

优化点:异步加载 JS、尽早加载关键资源

TTI - Time to Interactive

可交互时间,衡量页面加载完后到用户可以交互的时间值