最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 性能优化到底应该如何做

    正文概述 掘金(小丹子)   2021-05-14   776

    TL;DR: 当我们在做性能优化的时候,我们究竟在优化什么?做性能优化需不需要了解底层的东西?需要了解到什么程度?浏览器底层是一个什么架构?浏览器渲染的本质究竟是什么?哪些方面对用户的体验影响才是最大的?有没有业内一些通用的标准或标杆参考?都1202年了,雅虎军规还有没有用?性能分析工具都有哪些?我们从哪方面进行分析才是合适的?腾讯文档遇到过什么性能问题?

    本文为你一一解答这些问题。了解了这些问题,可能你在做性能优化的时候才能更加得心应手。

    目录:

    • 1. 性能优化的本质
      • 1.1 展示更快,响应更快
      • 1.2 理解浏览器多进程架构
      • 1.3 理解页面渲染相关进程
        • 1.3.1 Renderer Process & GPU Process
        • 1.3.2 Renderer Process详解
          • 1.3.2.1 Main Thread
    • 2. 看看经典:雅虎军规
    • 3. 性能指标
      • 3.1 什么样的性能指标才能真正代表用户体验?
      • 3.2 Core Web Vitals
        • 3.2.1 Largest Contenful Paint(LCP)
          • 3.2.1.1 LCP如何定义
          • 3.2.1.2 如何测量LCP
          • 3.2.1.3 如何优化LCP
        • 3.2.2 First Input Delay(FID)
          • 3.2.2.1 FID如何定义
          • 3.2.2.2 如何测量FID
          • 3.2.2.3 如何优化FID
        • 3.2.3 Cumulative Layout Shift(CLS)
          • 3.2.3.1 CLS如何定义
          • 3.2.3.2 如何测量CLS
          • 3.2.3.3 如何优化CLS
    • 4. 性能工具:共欲善其事,必先利其器
      • 4.1 Lighthouse
      • 4.2 PageSpeed Insights
      • 4.3 CrUX
      • 4.4 Chrome DevTools Performance面板
        • 4.4.1 Frame
        • 4.4.2 Timings
        • 4.4.3 Main
        • 4.4.4 Layers
        • 4.4.5 Rendering
          • 4.4.5.1 Paint flashing
          • 4.4.5.2 Layout Shift Regions
          • 4.4.5.3 Frame Rendering Stats
        • 4.4.6 Memory
      • 4.5 Search Console
      • 4.6 web.dev
      • 4.7 Web Vitals extension
      • 4.8 工具思考与总结
    • 5. 腾讯文档性能优化
      • 5.1 滑动性能优化
        • 5.1.1 过程
        • 5.1.2 总结
    • 6. 谈谈监控
    • 7. 总结

    1. 性能优化的本质

    1.1 展示更快,响应更快

    性能优化的目的,就是为了提供给用户更好的体验,这些体验包含这几个方面:展示更快、交互响应快、页面无卡顿情况。

    更详细的说,就是指,在用户输入url到站点完整把整个页面展示出来的过程中,通过各种优化策略和方法,让页面加载更快;在用户使用过程中,让用户的操作响应更及时,有更好的用户体验。

    对于前端工程师来说,要做好性能优化,需要理解浏览器加载和渲染的本质。理解了本质原理,才能更好的去做优化。所以我们先来看看浏览器架构是怎样的。

    1.2 理解浏览器多进程架构

    从大的方面来说,浏览器是一个多进程架构。

    它可以是一个进程包含多个线程,也可以是多个进程中,每个进程有多个线程,线程之间通过IPC通讯。每个浏览器有不同的实现细节,并没有标准规定浏览器必须如何去实现。

    这里我们只谈论chrome架构。

    下面这张图是目前chrome的多进程架构图。 性能优化到底应该如何做 (图片引自Mariko Kosaka的《Inside look at modern web browser》)

    我们来看看这些进程分别对应浏览器窗口中的哪一部分: 性能优化到底应该如何做 (图片引自Mariko Kosaka的《Inside look at modern web browser》)

    那么,怎么看浏览器对应启动了什么进程呢? chrome中,我们可以通过更多->More Tools->Task Manager看到启动的进程。 性能优化到底应该如何做

    从chrome官网和源码,我们也可以得知,多进程架构中包含这些进程:

    • Browser进程:打开浏览器后,始终只有一个。该进程有UI线程、Network线程、Storage线程等。用户输入url后,首先是Browser进程进行响应和请求服务器获取数据。然后传递给Renderer进程。
    • Renderer进程:每一个tab一个,负责html、css、js执行的整个过程。 前端性能优化也与这个进程有关
    • Plugin进程:与浏览器插件相关,例如flash等。
    • GPU进程:浏览器共用一个。主要负责把Renderer进程中绘制好的tile位图作为纹理上传到GPU,并调用GPU相关方法把纹理draw到屏幕上。

    这里的话只是简单介绍一下浏览器的多进程架构,让大家对浏览器整体架构有个初步认识,其实背后的细节还有很多,这里就不一一展开。有兴趣可以细看这一系列文章和chrome官网介绍。

    1.3 理解页面渲染相关进程

    1.3.1 Renderer Process & GPU Process

    从以上的多架构,我们了解到,与前端渲染、性能优化相关的,其实主要是Renderer进程和GPU进程。那么,它们又是什么架构呢?

    来看一下这张我们再熟悉不过的图。 性能优化到底应该如何做 (图片引自Paul的《The Anatomy of a Frame》)

    • Renderer进程:包括3个线程。合成线程(Compositor Thread)、主线程(Main Thread)、Compositor Tile Worker。
    • GPU进程:只有GPU线程,负责接收从Renderer进程中的Compositor Thread传过来的纹理,显示到屏幕上。

    1.3.2 Renderer Process详解

    Renderer进程中3个线程的作用为:

    • Compositor Thread:首先接收vsync信号(vsync信号是指操作系统指示浏览器去绘制新的帧),任何事件都会先到达Compositor线程。如果主线程没有绑定事件,那么Compositor线程将避免进入主线程,并尝试将输入转换为屏幕上的移动。它将更新的图层位置信息作为帧通过GPU线程传递给GPU进行绘制。

    当用户在快速滑动过程中,如果主线程没有绑定事件,Compositor线程是可以快速响应并绘制的 ,这是浏览器做的一个优化。

    • Main Thread: 主线程就是我们前端工程师熟知的线程,这里会执行解析Html、样式计算、布局、绘制、合成等动作。所以关于性能的问题,都发生在了这里。所以应该重点关注这里
    • Compositor Tile Worker:由合成线程产生一个或多个worker来处理光栅化的工作。

    Service Workers和Web Workers可以暂时理解也在Renderer进程中,这里不展开讨论。

    1.3.2.1 Main Thread

    性能优化到底应该如何做

    主线程需要重点讲下。因为这是我们的代码真实存在的环境。

    从上一小节Render进程和GPU进程的图中,我们可以看到有个红色的箭头,从Recal Styles和Layout指向了requestAnimationFrame,这意味着有Forced Synchronous Layout (or Styles)(强制回流和重绘)发生,这一点在性能方面特别要注意。

    在Main Thread中,有这几个需要注意一下:

    • requestAnimationFrame:因为布局和样式计算是在rAF之后,所以在rAF是进行元素变更的理想时机。如果在这里对一个元素变更100个类,不会进行100次计算,它们会分批以后处理。需要注意的是,不能在rAF中查询任何计算样式和布局的属性(例如:el.style.backgroundImage或el.style.offsetWidth),因为这样会导致重绘和回流。
    • Layout:布局的计算通常是针对整个文档的,并且与DOM元素的大小成正比!(这点特别要注意,如果一个页面DOM元素太多,也会导致性能问题)

    主线程的顺序始终都是:Input Event Handler->requestAnimationFrame->ParseHtml->ReculateStyles->Layout->Update Layer Tree->Paint->Composite->commit->requestIdleCallback,只能从前往后,例如,必须先是ReculateStyles,然后Layout、然后Paint。但是,如果它只需要做最后一步Paint,那么这就是它全部要做的事情,不会再发生前面的ReculateStyles和Layout。

    这里其实给了我们一个启示: 如果要让fps保持60,即每帧的js执行时间少于16.66ms,那么让这个主线程执行的过程尽可能地少,是我们的性能优化目标

    根据主线程的这些步骤,理想的情况下,我们只希望浏览器只发生最后一个步骤:Composite(合成)。

    CSS的属性是我们需要关注一下的模块。这里描述了哪些CSS属性会引起重绘、回流和合成。例如,让我们给一个元素进行移动位置时: transformopacity 可以直接触发合成,但是 lefttop 却会触发Layout、Paint、Composite3个动作。所以显然用transform时更好的方案。

    但这并不是说我们不应该用left和top这些可能引起重绘回流的属性,而是应该关注每个属性在浏览器性能中引起的效果

    2. 看看经典:雅虎军规

    多年前雅虎的Nicolas C. Zakas提出7个类别35条军规,至今为止很多前端优化准则都是围绕着这个展开。如果严格按照这些规则去做,其实我们有很多优化工作可以做,只要认真践行,性能提升不是问题。

    性能优化到底应该如何做

    我们来看看它7个分类都是围绕哪些方面展开:

    • Server:与页面发起请求的相关;
    • Cookie:与页面发起请求相关;
    • Mobile:与页面请求相关;
    • Content:与页面渲染相关;
    • Image:与页面渲染相关;
    • CSS:与页面渲染相关;
    • Javascript:与页面渲染和交互相关。

    从上面的描述可以看到,其实雅虎军规,是围绕页面发起请求那一刻,到页面渲染完成,页面开始交互这几个方面来展开,提出的一些原则。

    很多原则大家也都耳熟能详,就不全部展开了,有兴趣的同学可以去查看原文。这里主要想提一些忽略但是又值得注意的点:

    • 减少DOM节点数量

    为什么要减少DOM节点的数量? 当遍历查询500和5000个DOM节点,进行事件绑定时,会有所差别。 当一个页面DOM节点过多,应该考虑使用无限滚动方案来使视窗节点可控。可以看看google提的方案。

    • 减少cookie大小

    cookie 传输会造成带宽浪费,影响响应时间,可以这样做: 消除不必要的cookies; 静态资源不需要 cookie,可以采用其他的域名,不会主动带上 cookie。

    • 避免图片src为空

    图片src为空时,不同浏览器会有不同的副作用,会重新发起一起请求。

    3. 性能指标

    3.1 什么样的性能指标才能真正代表用户体验?

    思考点详细内容
    Is it happening?导航是否成功,服务器是否响应了Is it useful?是否已经渲染了足够的内容,让用户可以开始参与其中Is it usable?用户是否可以与页面交互,页面是否处于繁忙状态Is it delightful?交互是否流畅、自然、没有滞后反映或卡顿

    通常有2种途径来衡量性能。

    1. 本地实验衡量:本地模拟用户的网络、设备等情况进行测试。通常在开发新功能的时候,实验测量是很重要的,因为我们不知道这个功能发布到线上会有什么性能问题,所以提前进行性能测试,可以进行预防。
    2. 线上衡量:实验测量固然可以反映一些问题,但无法反映在用户那里真实的情况。同样的,在用户那里,性能问题会和用户的设备、网络情况有关,而且还跟用户如何与页面进行交互有关。

    有这几个类型与用户感知性能相关。

    • 页面加载时间:页面以多快的速度加载和渲染元素到页面上。
    • 加载后响应时间:页面加载和执行js代码后多久能响应用户交互。
    • 运行时响应:页面加载完成后,对用户的交互响应时间。
    • 视觉稳定性:页面元素是否会以用户不期望的方式移动,并干扰用户的交互。
    • 流畅度:过渡和动画是否以一致的帧率渲染,并从一种状态流畅地过渡到另一种状态。

    对应上面几种分类,Google和W3C性能工作组提供了对应这几种性能指标:

    • First contentful paint (FCP): 测量页面开始加载到某一块内容显示在页面上的时间。
    • Largest contentful paint (LCP): 测量页面开始加载到最大文本块内容或图片显示在页面中的时间。
    • First input delay (FID): 测量用户首次与网站进行交互(例如点击一个链接、按钮、js自定义控件)到浏览器真正进行响应的时间。
    • Time to Interactive (TTI): 测量从页面加载到可视化呈现、页面初始化脚本已经加载,并且可以可靠地快速响应用户的时间。
    • Total blocking time (TBT): 测量从FCP到TTI之间的时间,这个时间内主线程被阻塞无法响应用户输入。
    • Cumulative layout shift (CLS): 测量从页面开始加载到状态变为隐藏过程中,发生不可预期的layout shifts的累积分数。

    这些指标能从一定程度上衡量页面性能,但不一定都是有效的。举个例子。LCP指标主要用户衡量页面的主要内容是否完成加载,但会有这样的情况,最大的元素并不是主要内容,那么这个时候LCP指标并不是那么重要。 每个不同的站点有自己的特殊性,可以参考以上角度进行衡量,也需要因地制宜。

    3.2 Core Web Vitals

    在以上列出的指标中,Google定义了3个最核心的指标,作为Core Web Vitals。它们分别代表着:加载、交互、视觉稳定性。

    性能优化到底应该如何做

    • Largest Contentful Paint (LCP): 测量加载性能。为了能提供较好的用户体验,LCP指标建议页面首次加载要在2.5s内完成。
    • First Input Delay (FID): 测量交互性能。为了提供较好用户体验,交互时间建议在100ms或以内。
    • Cumulative Layout Shift (CLS): 测量视觉稳定性。为了提供较好用户体验,页面应该维持CLS在0.1或以内。

    当页面访问量有75%的数据达到了以上以上Good的标准,则认为性能是不错的了。

    Core Web Vitals是作为核心性能指标,但是其他指标也同样在重要,是做为核心指标的一个辅助。例如,TTFB和FCP都可以用来衡量加载性能(服务器响应时间和渲染时间),它们作为LCP的一个问题手段辅助。同样的,TBT和TTI对于衡量交互性能也很重要,是FID的一个辅助,但是它们无法在线上进行测量,也无法反映以用户为中心的结果。

    Google官方提供了一个web-vitals库,线上或本地都可以测量上面提到的3个指标:

    import {getCLS, getFID, getLCP} from 'web-vitals';
    
    function sendToAnalytics(metric) {
      const body = JSON.stringify(metric);
      // Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
      (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
          fetch('/analytics', {body, method: 'POST', keepalive: true});
    }
    
    getCLS(sendToAnalytics);
    getFID(sendToAnalytics);
    getLCP(sendToAnalytics);
    

    下面,分别讲讲这3个指标定义的原因、如何测量、如何优化。

    3.2.1 Largest Contentful Paint (LCP)

    3.2.1.1 LCP如何定义

    性能优化到底应该如何做 (图片来自LCP)

    LCP是指页面开始加载到最大文本块内容或图片显示在页面中的时间。那么哪些元素可以被定义为最大元素呢?

    • <img>标签
    • <image> 在svg中的image标签
    • <video> video标签
    • CSS background url()加载的图片
    • 包含内联或文本的块级元素

    3.2.1.2 如何测量LCP

    线上测量工具

    • Chrome User Experience Report
    • PageSpeed Insights
    • Search Console (Core Web Vitals report)
    • web-vitals JavaScript library

    实验室工具

    • Chrome DevTools
    • Lighthouse
    • WebPageTest

    原生的JS API测量 LCP还可以用JS API进行测量,主要使用PerformanceObserver接口,目前除了IE不支持,其他浏览器基本都支持了。

    new PerformanceObserver((entryList) => {
      for (const entry of entryList.getEntries()) {
        console.log('LCP candidate:', entry.startTime, entry);
      }
    }).observe({type: 'largest-contentful-paint', buffered: true});
    

    我们看一下结果是怎样的:

    性能优化到底应该如何做

    Google官方web-vitals库 Google官方也提供了一个web-vitals库,底层还是使用这个API,只是帮我们处理了一些需要测量和不需测量的场景、以及一些细节问题。

    3.2.1.3 如何优化LCP

    LCP可能被这四个因素影响:

    • 服务端响应时间
    • Javascript和CSS引起的渲染卡顿
    • 资源加载时间
    • 客户端渲染

    更加详细的优化建议就不展开了,可以参考这里。

    3.2.2 First Input Delay (FID)

    3.2.2.1 FID如何定义

    性能优化到底应该如何做 (图片来自FID)

    我们都知道第一印象的重要性,比如初次遇到某人形成的印象,会在后续交往中起重要的影响。对于一个网站也是如此。

    网站以多快的速度加载完成是其中一项指标,加载后以多快的速度对用户进行响应也同样重要。FID就是指后者。

    可以通过下面的图来更详细了解FID处于哪个位置:

    性能优化到底应该如何做 (图片来自FID)

    从上图可以看出,当主线程处于繁忙的时候,FID是指从浏览器接收到了用户输入,到浏览器对用户的输入进行响应的延迟时间。

    通常,当我们在写代码的时候,会认为只要用户输入信息,我们的事件回调就会立刻响应,但实际上并不是这样。这是主线程可能处于繁忙,浏览器正忙着解析和执行其他js。如上图所示的FID时间,主线程正在处理其他任务。

    当FID的时间为100ms或以内,则为Good。

    上面的例子中,用户刚好在主线程最繁忙的时刻进行了交互,但是如果用户在主线程空闲的时候交互,那么浏览器可以立刻响应。所以FID的值需要重点查看它的分布情况。

    FID实际上测量的是输入事件被感知到到主线程空闲的这段时间。这意味着即使没有输入事件被注册,FID也可以测量。因为用户的输入相应并不一定需要事件被执行,但一定需要主线程是空闲的。例如,下面这些HTML元素都需要在交互响应之前等待主线程上的正在执行的任务完成:

    • 输入框,例如<input><textarea><radio><checkbox>
    • 下拉框,例如<select>
    • 链接,例如<a>

    为什么要考虑测量第一次的输入延迟?有如下原因:

    • 因为第一次输入延迟是用户对你的网站形成的第一个印象,网站是否有质量且可靠;
    • 在今天,web中最大的交互问题第一次加载之后;
    • 对于网站应该如何解决较高的首次输入延迟(例如代码分割、减少JavaScript的预加载)的建议解决方案(TTI是指衡量这一块),不一定与在页面加载后解决输入延迟(FID是指衡量这一块)的解决方案相同。所以FID是在TTI的基础上更精确的细分。

    为什么FID只是包含从用户输入到主线程开始相应的时间?而没有包含事件处理到浏览器绘制UI的时间?

    尽管主线程处理和绘制的这段时间也很重要,但是如果FID把这段时间也包含进来,开发者可能会使用异步API(例如setTimeoutrequestAnimationFrame)来把这个task拆分到下一帧,以较少FID的时间,这样不仅没有提高用户体验,反而使用户体验降低。

    3.2.2.2 如何测量FID

    FID可以在实验环境也可以在线上环境测量。

    线上测量工具

    • Chrome User Experience Report
    • PageSpeed Insights
    • Search Console (Core Web Vitals report)
    • web-vitals JavaScript library

    原生的JS API测量

    new PerformanceObserver((entryList) => {
      for (const entry of entryList.getEntries()) {
        const delay = entry.processingStart - entry.startTime;
        console.log('FID candidate:', delay, entry);
      }
    }).observe({type: 'first-input', buffered: true});
    

    PerformanceObserver目前除了在IE上没有兼容,其他浏览器基本都兼容了。

    我们看一下结果是怎样的:

    性能优化到底应该如何做

    Google官方web-vitals库 Google官方也提供了一个web-vitals库,底层还是使用这个API,只是帮我们处理了一些需要测量和不需测量的场景、以及一些细节问题。

    3.2.2.3 如何优化FID

    FID可能被这四个因素影响:

    • 减少第三方代码的影响
    • 减少Javascript的执行时间
    • 最小化主线程工作
    • 减小请求数量和请求文件大小

    更加详细的优化建议可以参考这里。

    3.2.3 Cumulative Layout Shift (CLS)

    3.2.3.1 CLS如何定义

    性能优化到底应该如何做 (图片来自CLS)

    CLS是一个非常重要的、以用户为中心的测量指标。它能衡量页面是否排版稳定。

    页面移动会经常发生在资源异步加载、或者DOM元素动态添加到已存在的页面元素上面。这些元素有可能是图片、视频、第三方广告或小图标等。

    但是我们开发过程中可能不会察觉到这些问题,因为调试过程中刷新页面,图片都已经缓存在本地。调试接口的时候我们使用的是mock或者在局域网,接口速度都很快,这些延迟都可能被我们忽略。

    CLS就是帮我们去发现这些真实发生在用户端的问题的指标。

    CLS是测量页面生命周期中,每个发生意外布局移动的分数。当一个可视元素在下一帧移动到另外一个位置,就是指布局移动。

    CLS的分数在0.1或以下,则为Good。

    那么意外布局移动的分数如何计算?

    浏览器会监控两桢之间发生移动的不稳定元素。布局移动分数由2个元素决定:impact fraction和distance fraction。

    layout shift score = impact fraction * distance fraction
    

    可视区域内,在前一帧到下一帧之间所有不稳定的元素的并集,会影响当前帧的布局移动分数。

    举个例子,下面这张图中,左边是当前帧的一个元素,下一帧中,元素下移了可视区域内25%的高度。红色虚线框标出了两桢中当前元素的并集,占适口的75%,所以这个时候,impact faction是0.75。

    另外一个影响布局移动分数的是distance fraction,指这个元素相对视口移动的距离。不管是横向还是竖向,取最大值。

    下面例子中,竖向距离更大,该元素相对适口移动了25%的距离,所以distance fraction是0.25。所以布局移动分数是 0.75 * 0.25 = 0.1875.

    性能优化到底应该如何做

    但是要注意的是,并不是所有的布局移动都是不好的,很多web网站都会改变元素的开始位置。只有当布局移动是非用户预期的,才是不好的

    换句话说,当用户点击了按钮,布局进行了改动,这是ok的,CLS的JS API中有一个字段hadRecentInput,用来标识500ms内是否有用户数据,视情况而定,可以忽略这个计算。

    3.2.3.2 如何测量CLS

    线上测量工具

    • Chrome User Experience Report
    • PageSpeed Insights
    • Search Console (Core Web Vitals report)
    • web-vitals JavaScript library

    实验室工具

    • Chrome DevTools
    • Lighthouse
    • WebPageTest

    原生的JS API测量

    let cls = 0;
    
    new PerformanceObserver((entryList) => {
      for (const entry of entryList.getEntries()) {
        if (!entry.hadRecentInput) {
          cls += entry.value;
          console.log('Current CLS value:', cls, entry);
        }
      }
    }).observe({type: 'layout-shift', buffered: true});
    

    我们看一下结果是怎样的:

    性能优化到底应该如何做

    Google官方web-vitals库 Google官方也提供了一个web-vitals库,底层还是使用这个API,只是帮我们处理了一些需要测量和不需测量的场景、以及一些细节问题。

    3.2.3.3 如何优化CLS

    我们可以根据这些原则来避免非预期布局移动:

    • 图片或视屏元素有大小属性,或者给他们保留一个空间大小,设置width、height,或者使用unsized-media feature policy。
    • 不要在一个已存在的元素上面插入内容,除了相应用户输入。
    • 使用animation或transition而不是直接触发布局改变。

    更详细的内容可以看这里。

    4. 性能工具:工欲善其事,必先利其器

    Google开发的所有工具都支持Core Web Vitals的测量。工具如下:

    • Lighthouse
    • PageSpeed Insights
    • Chrome DevTools
    • Search Console
    • web.dev's提供的测量工具
    • Web Vitals扩展
    • Chrome UX Report API

    性能优化到底应该如何做

    这些工具对Core Web Vitals的支持如下:

    性能优化到底应该如何做

    4.1 Lighthouse

    打开F12,就可以看到Lighthouse,点击Generate Report,即可生成报告。当然也可以添加chrome插件使用。

    性能优化到底应该如何做

    Lighthhouse是一个实验室工具,本地模拟移动端和PC端对这几个方面进行测试。同时lighthouse还会针对这几个方面提出建议,在产品上线前值得一测。

    性能优化到底应该如何做

    Lighthouse还提供了Lighthouse CI,把Lighthouse集成到CI流水线中。举个例子,每次在上线之前,跑50次流水线对Lighthouse的各项指标进行测试取平均值,一旦发现异常,立刻进行排查。把性能问题排查提前到发布之前。这块后面会细讲。

    4.2 PageSpeed Insights

    PageSpeed Insights(PSI)是一个可以分析线上和实验室数据的工具。它是根据线上环境用户真实的数据(在Chrome UX 报告中)和Lighthouse结合出一份报告。和Lighthouse类似,它也会给出一些分析建议,可以知道页面的Core Web Vitals是否达标。

    性能优化到底应该如何做

    PageSpeed只是提供对单个页面的性能测试,而Search Console是正对整个网站的性能测试。

    PageSpeed Insights也提供了API供我们使用。同样的,我们也可以把它集成到CI中。

    4.3 CrUX

    Chrome UX Report (CrUX)是指汇聚了成千上万条用户体验数据的数据报告集,它是经过用户同意才进行上报的,目前存储在Google BigQuery上,可以使用账号登陆进行查询。它测量了所有的Core Web Vitals指标。

    上面提到的PageSpeed Insights工具就是结合CrUX的数据进行分析给出的结论。

    当然CrUX现在也提供了API共我们进行查询,可以查询的数据包括:

    • Largest Contentful Paint
    • Cumulative Layout Shift
    • First Input Delay
    • First Contentful Paint

    原理如下:

    性能优化到底应该如何做

    通过API的查询的数据每日都更新,并汇集了过去28天的数据。

    具体的使用方式可以参考官方给出的demo。

    4.4 Chrome DevTools Performance面板

    Performance是我们最常用的本地性能分析工具。

    性能优化到底应该如何做

    这里像提几点可以关注下的功能:Frame、Timings、Main、Layers、FPS。下面一一讲解。

    4.4.1 Frame

    点击Frame展开后,会看到有一个一个红色或绿色小块,这些代表着每帧的消耗时间。目前大多数设备的屏幕刷新率为 60 次/秒,浏览器渲染页面的每一帧的速率如果与设备屏幕的刷新率保持一致,即60fps时,我们是不会感知到页面卡的情况的。

    我们把鼠标移上去看看:

    性能优化到底应该如何做

    这种体验顺畅的情况。

    再比如: 性能优化到底应该如何做

    提示这一帧耗时了30.9ms,当前是32fps并且是掉帧状态。

    4.4.2 Timings

    这里可以看到几个关键指标的时间点。

    • FP:First Paint;
    • FCP:First Contentful Paint;
    • LCP:Largest Contenful Paint;
    • DCL:DOMContentLoaded Event
    • L:OnLoad Event。

    性能优化到底应该如何做

    4.4.3 Main

    Main是DevTools中最常用也是最重要的功能。

    性能优化到底应该如何做

    通过record,我们可以查看页面上所有操作在主线程中的执行过程。也就是我们常说的流程:

    性能优化到底应该如何做

    一旦有任何一个流程时间过长或频繁发生,比如Update Layer Tree时间过长、频繁出现RecalcStyles、Layout(重绘回流),那么需要引起注意。后面会举一个例子。

    4.4.4 Layers

    Layers是浏览器在绘制过程中生成的一个层。因为浏览器底层渲染的本质是纵向分层、横向分块。这一块的知识点是发生在Renderer Process进程中。后面会以一个例子展开讲。

    这里想提Layers的原因是,Layer的渲染也会影响性能问题,而且有时候还不容易被发现!

    Layers面板一般不会默认展示出来,点击更多->more tools->Layers即可打开。

    性能优化到底应该如何做

    点击Layers面板,点击左边下三角展开按钮,可以看见页面最终生成的合成层。右边左上角可以选择不同纬度进行查看。

    性能优化到底应该如何做

    选中某个层,可以查看该层生成的原因。

    性能优化到底应该如何做

    Chrome的Blink内核给出了54种会生成合成层的原因:

    constexpr CompositingReasonStringMap kCompositingReasonsStringMap[] = {
        {CompositingReason::k3DTransform, "transform3D", "Has a 3d transform"},
        {CompositingReason::kTrivial3DTransform, "trivialTransform3D",
         "Has a trivial 3d transform"},
        {CompositingReason::kVideo, "video", "Is an accelerated video"},
        {CompositingReason::kCanvas, "canvas",
         "Is an accelerated canvas, or is a display list backed canvas that was "
         "promoted to a layer based on a performance heuristic."},
        {CompositingReason::kPlugin, "plugin", "Is an accelerated plugin"},
        {CompositingReason::kIFrame, "iFrame", "Is an accelerated iFrame"},
        {CompositingReason::kSVGRoot, "SVGRoot", "Is an accelerated SVG root"},
        {CompositingReason::kBackfaceVisibilityHidden, "backfaceVisibilityHidden",
         "Has backface-visibility: hidden"},
        {CompositingReason::kActiveTransformAnimation, "activeTransformAnimation",
         "Has an active accelerated transform animation or transition"},
        {CompositingReason::kActiveOpacityAnimation, "activeOpacityAnimation",
         "Has an active accelerated opacity animation or transition"},
        {CompositingReason::kActiveFilterAnimation, "activeFilterAnimation",
         "Has an active accelerated filter animation or transition"},
        {CompositingReason::kActiveBackdropFilterAnimation,
         "activeBackdropFilterAnimation",
         "Has an active accelerated backdrop filter animation or transition"},
        {CompositingReason::kXrOverlay, "xrOverlay",
         "Is DOM overlay for WebXR immersive-ar mode"},
        {CompositingReason::kScrollDependentPosition, "scrollDependentPosition",
         "Is fixed or sticky position"},
        {CompositingReason::kOverflowScrolling, "overflowScrolling",
         "Is a scrollable overflow element"},
        {CompositingReason::kOverflowScrollingParent, "overflowScrollingParent",
         "Scroll parent is not an ancestor"},
        {CompositingReason::kOutOfFlowClipping, "outOfFlowClipping",
         "Has clipping ancestor"},
        {CompositingReason::kVideoOverlay, "videoOverlay",
         "Is overlay controls for video"},
        {CompositingReason::kWillChangeTransform, "willChangeTransform",
         "Has a will-change: transform compositing hint"},
        {CompositingReason::kWillChangeOpacity, "willChangeOpacity",
         "Has a will-change: opacity compositing hint"},
        {CompositingReason::kWillChangeFilter, "willChangeFilter",
         "Has a will-change: filter compositing hint"},
        {CompositingReason::kWillChangeBackdropFilter, "willChangeBackdropFilter",
         "Has a will-change: backdrop-filter compositing hint"},
        {CompositingReason::kWillChangeOther, "willChangeOther",
         "Has a will-change compositing hint other than transform and opacity"},
        {CompositingReason::kBackdropFilter, "backdropFilter",
         "Has a backdrop filter"},
        {CompositingReason::kBackdropFilterMask, "backdropFilterMask",
         "Is a mask for backdrop filter"},
        {CompositingReason::kRootScroller, "rootScroller",
         "Is the document.rootScroller"},
        {CompositingReason::kAssumedOverlap, "assumedOverlap",
         "Might overlap other composited content"},
        {CompositingReason::kOverlap, "overlap",
         "Overlaps other composited content"},
        {CompositingReason::kNegativeZIndexChildren, "negativeZIndexChildren",
         "Parent with composited negative z-index content"},
        {CompositingReason::kSquashingDisallowed, "squashingDisallowed",
         "Layer was separately composited because it could not be squashed."},
        {CompositingReason::kOpacityWithCompositedDescendants,
         "opacityWithCompositedDescendants",
         "Has opacity that needs to be applied by compositor because of composited "
         "descendants"},
        {CompositingReason::kMaskWithCompositedDescendants,
         "maskWithCompositedDescendants",
         "Has a mask that needs to be known by compositor because of composited "
         "descendants"},
        {CompositingReason::kReflectionWithCompositedDescendants,
         "reflectionWithCompositedDescendants",
         "Has a reflection that needs to be known by compositor because of "
         "composited descendants"},
        {CompositingReason::kFilterWithCompositedDescendants,
         "filterWithCompositedDescendants",
         "Has a filter effect that needs to be known by compositor because of "
         "composited descendants"},
        {CompositingReason::kBlendingWithCompositedDescendants,
         "blendingWithCompositedDescendants",
         "Has a blending effect that needs to be known by compositor because of "
         "composited descendants"},
        {CompositingReason::kPerspectiveWith3DDescendants,
         "perspectiveWith3DDescendants",
         "Has a perspective transform that needs to be known by compositor because "
         "of 3d descendants"},
        {CompositingReason::kPreserve3DWith3DDescendants,
         "preserve3DWith3DDescendants",
         "Has a preserves-3d property that needs to be known by compositor because "
         "of 3d descendants"},
        {CompositingReason::kIsolateCompositedDescendants,
         "isolateCompositedDescendants",
         "Should isolate descendants to apply a blend effect"},
        {CompositingReason::kFullscreenVideoWithCompositedDescendants,
         "fullscreenVideoWithCompositedDescendants",
         "Is a fullscreen video element with composited descendants"},
        {CompositingReason::kRoot, "root", "Is the root layer"},
        {CompositingReason::kLayerForHorizontalScrollbar,
         "layerForHorizontalScrollbar",
         "Secondary layer, the horizontal scrollbar layer"},
        {CompositingReason::kLayerForVerticalScrollbar, "layerForVerticalScrollbar",
         "Secondary layer, the vertical scrollbar layer"},
        {CompositingReason::kLayerForScrollCorner, "layerForScrollCorner",
         "Secondary layer, the scroll corner layer"},
        {CompositingReason::kLayerForScrollingContents, "layerForScrollingContents",
         "Secondary layer, to house contents that can be scrolled"},
        {CompositingReason::kLayerForSquashingContents, "layerForSquashingContents",
         "Secondary layer, home for a group of squashable content"},
        {CompositingReason::kLayerForForeground, "layerForForeground",
         "Secondary layer, to contain any normal flow and positive z-index "
         "contents on top of a negative z-index layer"},
        {CompositingReason::kLayerForMask, "layerForMask",
         "Secondary layer, to contain the mask contents"},
        {CompositingReason::kLayerForDecoration, "layerForDecoration",
         "Layer painted on top of other layers as decoration"},
        {CompositingReason::kLayerForOther, "layerForOther",
         "Layer for link highlight, frame overlay, etc."},
        {CompositingReason::kBackfaceInvisibility3DAncestor,
         "BackfaceInvisibility3DAncestor",
         "Ancestor in same 3D rendering context has a hidden backface"},
    };
    

    4.4.5 Rendering

    Rendering面板也隐藏了很多好用的功能。

    4.4.5.1 Paint flashing

    勾选了Paint flashing后,我们就会看到页面上有哪些内容被重绘了:

    性能优化到底应该如何做

    4.4.5.2 Layout Shift Regions

    勾选了Layout Shift Regions后,进行交互,就可以看到哪些元素进行了布局移动:

    性能优化到底应该如何做

    4.4.5.3 Frame Rendering Stats

    这个工具有一个小插曲。

    Frame Rendering Stats的前身是FPS meter,在Google版本85.0.4181.0改成了Frame Rendering Stats,但是迫于用户抱怨,在90版本的时候又改回来了。

    Frame Rendering Stats主要显示不掉帧率。而FPS侧重于显示每秒的刷新率fps。

    Chrome为什么要改成不掉帧率,是因为认为不掉帧率更能反映页面的顺畅度。而FPS显示每一秒渲染的帧数虽然能一定程度反映页面顺畅度,但是在一些特殊情况例如没有激活或空闲的页面,fps会比较低,这样并不能反映真实情况。

    性能优化到底应该如何做 (图片引自blink dev论坛讨论)

    4.4.6 Memory

    在大型项目中,内存问题也是有发生。DevTools也提供了内存分析工具供我们使用。

    点击Memory面板,点击录制按钮。

    性能优化到底应该如何做

    点击录制后,会看到当前状态下内存的占用情况,根据大小排序,我们可以定位到内存占用过多的地方。

    性能优化到底应该如何做

    4.5 Search Console

    Google Search Console其实就是监控和维护网站在 Google 搜索结果中的展示情况以及排查问题的平台。数据来源是CrUX。

    它会展示3个 Core Web Vitals metrics: LCP, FID, CLS。如果发现有问题,可以配合PageSpeed一起使用,分析问题。

    性能优化到底应该如何做 (图片引自vital-tools)

    4.6 web.dev

    web.dev/measure是google官方提供的测量性能工具,也会提供类似PageSpeed Insight的指标,还会提供一些具体代码更改建议。

    性能优化到底应该如何做 性能优化到底应该如何做

    4.7 Web Vitals extension

    Google也提供了扩展工具去测量Core Web Vitals。可以从Store中进行安装。

    性能优化到底应该如何做

    4.8 工具:思考与总结

    当我们了解了这么多工具之后,琳琅满目,我们该如何选择?如何使用好这些工具进行分析?

    • 首先我们可以使用Lighthouse,在本地进行测量,根据报告给出的一些建议进行优化;
    • 发布之后,我们可以使用PageSpeed Insights去看下线上的性能情况;
    • 接着,我们可以使用Chrome User Experience Report API去捞取线上过去28天的数据;
    • 发现数据有异常,我们可以使用DevTools工具进行具体代码定位分析;
    • 使用Search Console's Core Web Vitals report查看网站功能整体情况;
    • 使用Web Vitals扩展方便的看页面核心指标情况;

    5. 腾讯文档性能优化

    前面碎碎念念讲了这么多,终于来到了最后的实战环节。下面以腾讯文档为例子,讲讲我们定义了指标并且遇到过哪些比较难处理的性能问题。目前也在持续优化中。

    • 首屏

      • 首屏时间FCP:与Web Core Vitals定义的FCP类似,不过根据文档内容显示时间来自定义。
      • 可交互时间TTI:因为文档是在线编辑器,移动端下可交互我们认为是键盘可以弹起的时间
    • 内存:运行期间占用内存情况

    • crash:线上统计页面无响应率

    • fps:主要测量滑动fps

    • 卡顿

      • 运行期间不卡顿比例:衡量编辑期间不卡顿比例
      • 每打开一次文档出现卡顿次:编辑期间整体卡顿情况
    • 成功率

      • 白屏比例:页面白屏情况
      • js报错率:检查代码是否健康
      • 数据落盘成功率:数据是否保存成功

    从上面可以看出,我们主要围绕首屏加载、首屏可交互时间、交互情况、页面报错率、数据成功率、滑动顺畅度等维度进行优化的。

    5.1 滑动性能优化

    5.1.1 过程

    滑动性能的故事是这样子的。

    曾经,低端手机下,腾讯文档滑动很卡。通过竞品分析,我们发现石墨、飞书滑动非常顺畅。

    而移动端下,腾讯文档采用懒渲染方式,石墨飞书采用的是全量渲染,所以我们第一个怀疑方向是懒渲染。是不是因为懒渲染,js执行时间过长,主线程绘制过程有问题,导致加载慢,才导致滑动卡?带着这样的疑问,我们使用了全量渲染进行测试。但是,当全量渲染完成后,滑动依旧卡顿,使用perfdog测量的数据如下: 性能优化到底应该如何做

    这个时候,我们开始去研究源码,对比了下腾讯文档与石墨、飞书的滑动代码处理,发现了一个异常:石墨、飞书scroll事件断点后不会卡住,而腾讯文档会被卡住。

    那这个时候我们有2个疑问:

    • 疑问1: 打点后为什么可以滑动

    • 疑问2: 这个卡住会不会影响滑动性能

    如果我们不了解浏览器进程下多线程的架构,那这个时候肯定一脸懵逼,无从下手。

    性能优化到底应该如何做

    我们在来回顾一下这张图: 性能优化到底应该如何做

    前面有提到在Renderer进程中存在这样一个线程:Compositor Thread(合成线程)。这个线程主要干什么事情呢?

    Compositor Thread和Main Thread同样拥有绘制帧的信息。Compositor Thread会先接受到输入的vsync信号,处理滚动页面,不需要等待主线程执行完才commit给GPU线程绘制屏幕。

    快速滑动的时候,合成线程会快速处理屏幕显示,所以会看到低分辨率的信息。

    你会发现,合成线程可以优先处理滚动事件,而不需要等待主线程执行完才进行滚动,这也导致Touch事件可以执行e.preventDefault(),而scroll事件不能执行e.preventDefault()。(Chrome官网)

    到此为止,我们知道原来滑动断点卡住可能是因为Compositor Thread处理滑动失效。当我们把罪魁祸首找到,原来是workbench-float-widgets节点挡在编辑器节点上面,导致合成线程对scroll优化失效

    性能优化到底应该如何做

    当我们把这个节点层级调整到编辑器下面后,在进行滑动测量,似乎不会那么卡了。并且数据也有所提升:

    性能优化到底应该如何做

    滑动fps从20上升至40左右,但卡顿率依旧很高。

    那么我们继续分析,进行Performance录制。

    性能优化到底应该如何做

    发现一个异常,Update Layer Tree时间很长,Update Layer Tree又是一个什么东西?

    通过前面的学习,我们知道Update Layer Tree是跟层有关系的一个过程。那这个时候,我们需要更深入的理解Layer形成的过程。

    这里只做一个简单介绍,细节就不展开了,展开又是一个很长的篇幅。具体可以看Chrome的官方文档。

    浏览器渲染的本质是:纵向分层、横向分块。

    性能优化到底应该如何做

    从DOM Tree到Layer Tree会经过这样的过程:

    性能优化到底应该如何做 (图片来自Chrome的官方文档)

    最新的代码已经讲Render Object改为Layout Object,Render Layer改为Paint Layer了。

    所以Layer会经历从Render Layer到Graphics Layer的过程(当然,内核还会有其他Layer去处理中间态,这里我们就暂以官方文档的图为准):

    性能优化到底应该如何做

    上图中,CompositedLayerMapping就是我们前面提到的54个会提升为合成层的理由。RenderLayer是一个中间态,GraphicsLayer才是我们在Layer面板中看到的那个Layer。在看这个问题的时候,我们打开Layer面板,并没有看到很多Layer。

    我们再来看看官方文档是如何解释Layer生成的原因和过程的: 性能优化到底应该如何做

    我们注意到其中有一项,当CSS中有position:relative时,会生成Render Layer。于是乎我们找到了罪魁祸首。

    性能优化到底应该如何做

    当我们把这个问题解决后,滑动顺畅了很多,fps也提升了不少:

    性能优化到底应该如何做

    顺着这个Layer的问题,我们继续分析。滑动过程,我们打开Layer面板,发现滑动过程有很多个Layer出现。

    性能优化到底应该如何做

    性能优化到底应该如何做

    再一次的,通过上面的学习,我们已经知道Layer的过程,是先生成Render Layer,再生成Graphics Layer,如果这个过程中无法命中层压缩,那就会生成合成层,最终显示在Layer面板中。

    可以再次理解下这个过程:

    性能优化到底应该如何做

    命中无法层压缩原因:

    性能优化到底应该如何做

    至此,我们的滑动fps终于到达了58+。

    性能优化到底应该如何做

    5.1.2 总结

    通过这个案例,其实是想引申出以下几个思考点:

    • 当我们在做性能优化的时候,更多的应该从本质出发去思考这个问题为什么会存在;
    • 有没有什么工具辅助我们去快速定位出这个问题;
    • 认认真真把官方文档看一遍真的很重要,里面有很多隐藏的信息。知道了这些信息,会恍然大悟;

    6. 谈谈监控

    最后一个章节想来谈谈监控。

    我们在做性能优化的时候,常常会通过各种线上打点,来收集用户数据,进行性能分析。没错,这是一种监控手段,更精确的说,这是一种"事后"监控手段。

    **"事后"监控固然重要,但我们也应该考虑"事前"监控,否则,每次发布一个需求后,去线上看数据。咦,发现数据下降了,然后我们去查代码,去查数据,去查原因。这样性能优化的同学永远处于"追赶者"的角色,永远跟在屁股后面查问题。

    举个例子,我们可以这样去做"事前"监控。

    建立流水线机制。流水线上如何做呢?

    • Lighthouse CI 或 PageSpeed Insights API:把Lighthouse或PageSpeed Insights API集成到CI流水线中,输出报告分析。

    • Puppeteer 或 Playwright:使用E2E自动化测试工具集成到流水线模拟用户操作,得到Chrome Trace Files,也就是我们平常录制Performance后,点击左上角下载的文件。Puppeteer和Playwright底层都是基于Chrome DevTools Protocol。

    性能优化到底应该如何做

    • Chrome Trace Files:根据规则分析Trace文件,可以得到每个函数执行的时间。如果函数执行时间超过了一个临界值,可以抛出异常。如果一个函数每次的执行时间都超过了临界值,那么就值得注意了。但是还有一点需要思考的是:函数执行的时间是否超过临界值固然重要,但更重要的是这是不是用户的输入响应函数,与用户体验是否有关。

    性能优化到底应该如何做 (图片来自Flo Sloot的Jank: You can measure what your users can feel.)

    • 输出报告。定义异常临界值。如果异常过多,考虑是否卡发布流程。

    7. 总结

    终于到了我们的总结部分。 我们来回顾一下前面的内容:

    第一部分,讲了浏览器整体架构和渲染相关进程,为什么把这个章节也放到这篇性能优化的文章中?浏览器对于我们前端开发来说,是一个sandbox或者darkbox。我们知道js、html、css结合起来就能实现我们的需求,但如果知道它是如何去渲染、执行、处理我们的代码,不管是对做需求还是性能优化,都能更知其然和所以然。

    第二部分,雅虎军规是多年前提出的非常经典的优化建议。至今对于我们依然有很强的指导作用。你会发现它是从页面加载、页面渲染、到页面交互全面的一个指导建议。与今天Chrome和W3c提出的Web Vitals思路依然类似。

    第三部分,性能指标,参考标准与业内标杆的建议,能更好地指导我们进行优化。

    第四部分,性能工具。工欲善其事,必先利其器。这个道理大家都懂,运用好工具,才能让我们更加事半功倍。

    第五部分,以滑动性能优化为例子,让我们意识到理解原理(读书)的重要性。运用知识->结合工具->发现问题->发现本质问题->解决本质问题。

    第六部分,监控在性能优化中占很重要的部分,"事前"监控更重要,防患于未然。让性能优化成为一个预防者而不是追赶者。

    罗里吧嗦说了很多,当然还有很多性能优化的细节没有讲到,如果有错误的地方欢迎指正。或者有什么好方法好建议也强烈欢迎私聊交流一下~

    性能优化到底应该如何做

    参考文章: web.dev/learn-web-v…

    developers.google.com/web/updates…

    aerotwist.com/blog/the-an…

    www.chromium.org/developers/…

    medium.com/punching-pe…


    起源地下载网 » 性能优化到底应该如何做

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元