hawx1993 / tech-blog

📦My personal tech blog,not regularly update
http://sf.gg/u/trigkit4/articles
339 stars 30 forks source link

前端性能优化体系基础 #29

Open hawx1993 opened 1 year ago

hawx1993 commented 1 year ago

性能指什么?

前端的发展从刀耕火种到工业化再到自动化阶段,前端已然变得越来越庞大,大多数服务端的性能优化也会放到前端来做,优势显而易见,减少服务器压力,另一方面,对前端性能优化也会提出很大的要求。

一般来说用户可以感知的性能,包括加载速度、响应能力、动画流畅性、能耗以及内存占用等。比如:对于PWA大型单页面桌面应用而言,能耗和内存占用就是其需要时刻考虑优化的性能点,因为对于桌面端来说,用户很可能打开之后就一直放着,不会像网页一样随手关闭。如果内存没有及时回收掉,就难免产生内存泄露。相同的,能耗也会随之增加。
毫无疑问,性能的提升可以带来绝佳的用户体验,以及减少用户的“跳出率”,帮助网站留住更多用户。前端的发展也是伴随着性能优化的发展,下面我们看看前端性能优化的发展历程吧。

前端性能优化发展历程

最早的性能优化指导思想来自于 07年雅虎的 前端性能优化34 条军规,侧重于性能最佳实践。随后08年webpage test开源。09年谷歌提出SPDY协议,后来演进为我们现在使用的 HTTP2 标准。
解决方案方面,2010 年,Facebook 研究科学家 Changhao Jiang 提出 BigPie 技术,通过让浏览器与服务器并行工作来降低加载时间。接着在2016 年,谷歌在 I/O 大会提出 PWA 概念,可以增强web应用体验,做到离线缓存,消息通知等更接近原生应用的能力。最后在2020年,谷歌提出了 Web Vitals,Web Vitals 只是一项倡议,旨在为开发者提供统一的性能度量标准。

标准的发展和介绍

2010 年,w3c 成立了 web 性能工作组,web 性能工作组的目标就是制定衡量 web 应用性能的方法和API。主要氛围框架类API、度量类API和优化策略API。红色表示实验中的API。


下面介绍一些比较常用的API:

Performance.timing

如果大家有研究过前端性能监控,可能会知道浏览器会提供这么一个 API 叫 performance.timing,它会提供一个页面,从开始加载一直到加载完毕,中间各个阶段的一个模型,但这个 API 已经“废弃”了。为什么会被废弃?因为 W3C 给我们提供了更全面、更强大的一个性能分析矩阵,比单一的 performance.timing 更加强大,能帮助我们从各个方面分析前端页面性能。
这个 API 非常强大,但是并不适用所有场景 。比如:使用 window.performance.timing 所获的数据,在单页应用中改变 URL 但不刷新页面的情况下(单页应用典型路由方案),是不会更新的,还需要开发者重新设计统计方案。

Paint Timing

用于收集FP和FCP两个指标:

var observer = new PerformanceObserver(list => {
      var perfEntries = list.getEntries();
      for (var i = 0; i < perfEntries.length; i++) {
        console.log('perfEntries', perfEntries[i]);
      }
    });
observer.observe({ entryTypes: ['paint'] });

打印一下,输出结果如下:

从截图中可以获取first pain和First contentful paint 时间。

Resource Timing

资源计时API。通过访问performance.getEntriesByType('resource') 返回的数组对象,可以遍历当前页面所包含的资源加载相关的性能信息。

// 用法
performance.getEntriesByType('resource')

可以获取到资源加载的各个阶段的时间戳,如重定向、DNS查询、TCP连接建立。如下图所示:

Element Timing

该接口提供了监控图像元素和文本节点在屏幕上出现时的加载性能的功能。

<p elementtiming="text" id="text-id">text here</p>
  <script>
      const observer = new PerformanceObserver((list) => {
      let entries = list.getEntries().forEach(function (entry) {
      console.log(entry);
    });
  });
 observer.observe({ entryTypes: ["element"] });
</script>

面向用户端的优化体系

对于用户的真实感受,我们仍缺少一个可以量化的标准去衡量,如何更准确的衡量用户的真实感受,我们可以看看一些最新的指标,用户指标概览:

用户指标概览

首先网页在用户端的呈现过程,用户最关心的一定是网页加载是不是足够快,时间太久,用户往往会失去耐心而离开。其次是用户的输入是否得到响应,最后如果网页内容在呈现过程中出现较大的抖动,或者视窗内容发生了较大偏移,这个时候用户的感受往往是不愉悦的。而这些指标被Google量化成了上述指标,可通过Lighthouse或Chrome Performance来检测。
核心 Web 指标是适用于所有网页的 Web 指标子集,每位网站所有者都应该测量这些指标,当前针对 2020 年的指标构成侧重于用户体验的三个方面——加载性能、交互性和视觉稳定性——并包括以下指标:

TTI和TBT

TTI页面可交互时间


TTI指的是应用在视觉上都已渲染出了,完全可以响应用户的输入了。是衡量应用加载所需时间并能够快速响应用户交互的指标
统计方式:谷歌实验室写的npm包,tti-polyfill

import ttiPolyfill from 'tti-polyfill';

ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
  ga('send', 'event', {
    eventCategory:'Performance Metrics',
    eventAction:'TTI',
    eventValue: tti,
    nonInteraction: true,
  });
});

实现原理:整个TTI就是在一个5s内没有新请求或者新的js长任务的状态下, 找最后一个长任务的节点(可通过PerformanceObserver来监听长任务变化),如果长任务不存在用domContentLoadedEventEnd节点来当做可交互时间点。

TBT页面总阻塞时间

总阻塞时间 (TBT) 指标测量First Contentful Paint 首次内容绘制 (FCP)与Time to Interactive 可交互时间 (TTI)之间的总时间,这期间,主线程被阻塞的时间过长,无法作出输入响应。例如,如果 Lighthouse 检测到一个 70 毫秒长的任务,则阻塞部分将为 20 毫秒。所以,超出50ms,才会被认为是阻塞时间。TBT 应控制在300ms以内为佳。那么,如何检测TBT呢?
我们可以打开Performance标签,将CPU和network做节流,找到main部分,出现很长的红色任务条就是long task。当你将鼠标悬停在它上面时,你会看到它在主线程上占用了多少时间:

TTI和TBT区别

TTI 告诉您页面完全交互需要多长时间, TBT 衡量 FCP 和 TTI 之间发生的情况。如下图所示:

用户指标优化

LCP优化

LCP主要受以下四方面影响:

鉴于此,我们可以总结出如下优化策略:

FCP优化

FCP是指首次有内容绘制,鉴于此,我们可以总结出如下优化策略:

FID优化

FID衡量的是用户输入到主线程空闲的时间差,该项指标需要真实的用户交互才能测量响应延迟。这也是Chrome Lighthouse没有该指标而是使用TBT(总阻塞时间)来代替。每当出现长任务,浏览器主线程都被视作"阻塞状态"。那么,什么才是导致FID很差的罪魁祸首呢?主要有以下几项:


鉴于此,我们可以总结出如下优化策略:

其他优化方式

使用workerize优化Long Task

我们可以使用Chrome Dev tools 分析long task,long task是指耗时大于50ms的任务,会阻塞主线程渲染的任务。下面我们可以通过workerize来演示如何优化长任务。
workerize 是一个web worker npm包,可以用来将一个模块移动到web worker中执行。我们来看一个demo:

const App = () => {
  function add(a, b) {
    // block for half a second to demonstrate asynchronicity
    let start = Date.now();
    while (Date.now() - start < 1500);
    return a + b;
  }
  useEffect(() => {
    let worker = workerize(`
        export function add(a, b) {
            // block for half a second to demonstrate asynchronicity
            let start = Date.now();
            while (Date.now()-start < 1500);
            return a + b;
        }
    `);

    (async () => {
      console.log('3 + 9 = ', await worker.add(3, 9));
      console.log('1 + 2 = ', await worker.add(1, 2));
    })();
  }, []);
  return <>123</>;
};

export default App;

我们这里对CPU统一做了6x节流。我们发现如果不使用workerize,而是直接调用add方法,我们可以在Performance中发现一个长任务耗时3s,找到其中具体的方法add,可以在event log中发现self time接近1.5s的方法,点击右侧的链接可以直接跳转到source code。


接下来我们使用worker.add来调用,可以发现那个耗时3s的长任务已经没了,只有一个耗时200ms的任务。说明这个长任务已经被优化了。

对比前后两次的一个加载情况,我们可以发现没优化前,长任务是会阻塞渲染,影响FID,优化完之后,渲染进程就变快了许多。

使用Performance分析代码使用率

我们还可以通过 Chrome 提供的Coverage 工具分析运行时的代码使用情况:

小结:资源加载的性能优化可以用 Coverage 工具记录代码使用情况,分析出没用到的代码,使用 treeshking、懒加载等方式,针对性的优化它。

利用SSR技术加速首屏渲染

我们可以打开nextjs官网,点击showcase 可以看到基于nextjs的一些案例,我们打开https://xw.qq.com/,可以发现在网络请求中,在返回的html页面,在response中搜索<body>标签,我们可以看到页面显示的内容body标签都有
或者打开https://heapdump.cn/,也可以看到headDump性能社区也是服务端渲染的。
对比客户端渲染,我们可以打开tuba http://tuba.perfma-inc.com/,可以看到只有一个div标签,也就是页面所有内容都是动态去渲染出来的,也就是js解析之后,它会根据我们的需要,对id为root的标签内容去进行替换并插入相关内容。而服务端渲染实际上在后端已经渲染好了,直接把html返回给前端。

前端性能优化主要指标和工具

首屏时间和白屏时间

想要量化统计网页加载速度是一件很麻烦的事,尤其是想要做成一个通用化的工具,这个指标就基本只能靠猜测,即便是谷歌,也不能百分百保证猜得准。
网页加载速度有两个主要指标,一个是白屏时间,另一个则是首屏时间。
白屏时间即First  Paint ,首次渲染时间,在Chrome等现代化浏览器提供的Performance Timing API中也提供类似的FirstPaint 的指标。
对于网站请求来说,通常页面加载过程会产生页面URL本身、JS,CSS等静态资源请求、静态图片请求、各种站内Ajax请求和站外请求,域名解析可能短至几十毫秒,也可能消耗几秒,所以解析过程的快慢直接影响页面整体加载的渲染速度。
所以影响白屏时间的因素主要有:网络,服务端性能,前端页面结构设计。
首屏时间代表页面在第一个屏幕中的内容完全加载出来的时间。如下图所示:

与白屏不同的是,我们并不能精准的从浏览器系统API获取首屏时间,尽管没有公认的标准能够在所有情况下完美反应页面加载时间,First Paint和First Contentful Paint仍然为衡量页面加载时间提供了极具价值的数据。
影响首屏时间的因素:白屏时间,资源下载执行时间。

FP和FCP区别

FP(First Paint)和FCP(First Contentful Paint)的区别在于“内容”绘制。内容"指的是文本、图像、元素或非白色的元素。可以作为首屏时间。而FP则是白屏时间,只要有像素绘制即可作为白屏时间的结束时间点。FP 事件在 Graphic Layer 进行绘制的时候触发,而不是文本、图片或 Canvas 绘制的时候。
我们可写个简单demo展示一下两者的区别,FCP延迟3s执行,通过performance.getEntriesByType('paint') 可获取渲染时间FP和FCP的值如下:

import { useState, useEffect } from 'react';
import './App.css';

const App = () => {
  const [showFcp, setShowFcp] = useState(false);
  const renderFcp = () => {
    if (showFcp) {
      return <div className='fcp'>首次内容绘制FCP</div>;
    }
    return null;
  };
  useEffect(() => {
    setTimeout(() => {
      setShowFcp(true);
    }, 3000);
  }, []);
  return (
    <>
      <div className='bg-fp'></div>
      {renderFcp()}
    </>
  );
};

export default App;


上述例子可以看出用FP统计白屏时间会比FCP来的更精准些。而我们通过Lighthouse和Chrome Performance 统计到的指标值也近乎一致:

Web vitals和Lighthouse

Core Web Vitals和Lighthouse这两个工具都是由Google引入的,区别在于:Core Web Vitals引入的三个指标是LCP(最大内容绘制)、FID(首次输入延迟)、CLS(累积布局偏移),而Google Lighthouse也有三个指标,其中两个与核心Web的关键指标相同,分别是LCP(最大内容绘制)、CLS(累积布局偏移)、TBT(总阻塞时间)。由于Lighthouse集成在浏览器端,由于无法模拟用户输入,所以不存在FID这个指标。可以用总阻塞时间 (TBT) 作为代理,能够改进 TBT 的优化也应该能改进实际情况下的 FID。
另外,如果你发现两者数值偏差较大,很可能是Lighthouse受制于环境因素,比如开启了throttle或者统计成了移动端,移动端Lighthouse会自动模拟移动端弱网环境,进行较大程度的节流限制。而通过npm引入的web-vitals则不存在这些因素影响。

Lighthouse指标权重

性能分数是指标分数的加权平均值。自然,权重越大的指标对您的整体绩效得分的影响越大。指标分数在报告中不可见,但在后台计算。
https://googlechrome.github.io/lighthouse/scorecalc/#FCP=3000&SI=5800&FMP=4000&TTI=7300&FCI=6500&LCP=4000&TBT=600&CLS=0.25&device=desktop&version=6


诊断部分列出了开发人员可以探索以进一步提高其性能的其他指南。

前端性能监控与数据上报

从技术方面来讲,前端性能监控主要分为两种方式,一种叫做合成监控,另一种是真实用户监控。

合成监控

合成监控是从用户的角度监控应用程序、页面、API 等的有效工具,因此您可以更好地了解它们在用户面前的表现。
合成监控中最近比较流行的是 Google 的 Lighthouse,启动 Lighthouse 共有 4 种姿势,分别是 Chrome 开发者工具,Chrome 扩展程序,Node CLI 和 Node module。

真实用户监控

所谓真实用户监控,就是用户在我们的页面上访问,访问之后就会产生各种各样的性能指标,我们在用户访问结束的时候,把这些性能指标上传到我们的日志服务器上,进行数据的提取加工,最后在我们的监控平台上进行展示的一个过程。

数据上报方式

相关阅读

1、https://github.com/w3c/paint-timing
2、https://web.dev/measure/