前端性能优化方法论

90 阅读11分钟

一、目标概述

  1. 加快页面展示和运行速度,提升用户体验
  2. 节约服务器带宽流量,减少服务器压力
  3. 增强页面利用率和可访问性
  4. 提升浏览器排名

二、选择合适的性能指标

不同的应用程序需要根据不同的业务需求与用户体验选择不同的性能指标。 常用的具体性能指标如下:

  1. Time to Interactive (TTI) ,布局已经稳定、关键的Web字体可见、并且主线程已经空闲下来可以处理用户输入的时间点-基本上就是用户可以与UI交互的时间标记。这是了解用户必须经历多少等待才能毫无延迟地使用网站的关键指标。
  2. First Input Delay (FID),或者 Input responsiveness 从用户首次与网站进行交互到浏览器实际上能够响应该交互的时间。它很好地补充了TTI指标缺少的那部分:当用户实际与站点进行交互时会发生什么。 一般仅用作RUM度量标准。有一个标准的JavaScript库github.com/GoogleChrom…
  3. Largest Contentful Paint (LCP) ,这是在页面加载时间线中,标记已加载页面重要内容的时间点。假设页面中最重要的元素是在用户的视区中可见的最大元素。如果元素同时在可视区域的上方和下方渲染,只有可见部分被认为是相关的。
  4. Total Blocking Time (TBT) ,有助于量化页面在变为可靠交互之前处于非交互状态的严重程度(也就是说,主线程至少在5s内没有运行超过50ms的任务(长任务))。度量的是从第一次绘画(FP)到交互时间(TTI)之间的时间长短(在这段时间内,主线程被阻塞足够长的时间而无法响应用户输入)。所以说,低的TBT是良好性能的体现。
  5. Cumulative Layout Shift(CLS) ,该指标突出显示了用户访问网站时多久遇到一次意外的版式变化(重排)。它研究了不稳定因素及其对整体体验的影响。分数越低越好。
  6. Speed Index,度量页面内容可视化填充的速度,分数越低越好。速度指数分数是根据视觉填充的速度计算的,但它只是一个计算值。它对视口大小也很敏感,所以您需要定义一系列与您的目标用户是被相匹配的测试配置。随着LCP出现,它变得不那么重要了。
  7. CPU time spent ,这是显示主线程被阻塞的频率和时间的度量指标,受到浏览器绘制、渲染、加载和执行JS等动作的影响。高CPU响应时间是不稳定体验的明显指标,也就是说,这时候用户会体验到他们的操作和响应之间存在明显的滞后。使用WebPagetest,您可以在“Chrome”选项卡上选择“Capture Dev Tools Timeline”,以显示在使用WebPagetest的任何设备上运行时,主线程的被阻塞的具体细节。
  8. Component-Level CPU Costs,类似上面的 CPU time spent,这个由Stoyan Stefanov提出的指标主要用于度量JavaScript对CPU的影响。这个指标的想法是使用每个组件的CPU指令计数来独立地了解每个组件中Javascript其对整体体验的影响。可以[使用Puppeteer和Chrome来测量]((calendar.perfplanet.com/2019/javasc… "使用Puppeteer和Chrome来测量"))。
  9. FrustrationIndex(沮丧指数),它衡量最终用户感知到的关键里程碑,例如 标题可见、第一个内容可见、视觉上准备就绪、页面准备就绪,并计算一个分数,指示加载页面时的沮丧程度。间隔时间越大,用户感到沮丧的可能性就越大。对于用户体验而言,这可能是一个很好的KPI。
  10. Ad Weight Impact,如果站点依赖于广告产生的收入,那么跟踪与广告相关的代码大小是很有用的。
  11. Deviation metrics(偏差指标),结果中存在多少差异的数据可以侧面反映测量仪器的可靠性,以及应该对偏差和异常值给予多大的关注。较大的差异表明测试仪器的设置中需要进行调整。它还有助于了解某些页面是否更难以可靠地衡量,例如 “由于第三方脚本会导致重大变化”。跟踪浏览器版本也是一个好主意,这样可以方便地了解新版本的浏览器对性能的提高。
  12. Custom metrics,自定义指标的定义来源于业务需求和客户体验。它要求明确页面中重要的像素、关键脚本、必要的CSS和相关资源,并衡量它们对用户感知速度的影响。对于这一点,可以使用Performance API,为非常重要的事件标记特定的时间戳。 此外,还可以通过在测试结束时执行任意JavaScript来[使用WebPagetest收集自定义指标](collect custom metrics with WebPagetest "使用WebPagetest收集自定义指标") www.webpagetest.org/。

三、设定性能优化目标

为了让交互感觉流畅,页面响应用户的输入动作最好小于100ms。如果超过这个时间,用户就会觉得这个页面很迟钝。

另外,动画的每一帧都应在16毫秒内完成,从而达到每秒60帧(1秒÷60 = 16.6毫秒)- 最好在10毫秒以内。由于浏览器需要时间才能在屏幕上绘制新的帧,因此代码应在16.6毫秒之前完成执行。

根据上文选择指标,针对选择的指标设定目标,如: 3G环境下TTI时间< 5s,Speed Index < 3s

四、性能优化的思路

前端性能分两部分:

  1. 网络性能,减少网络时间,高效利用缓存
  2. 运行时性能
  • 重点关注影响用户体验的部分,避免提前优化,过度优化,陷入优化自嗨中,越抽象化的东西可读性越差,甚至可维护性也同时变差。

  • 影响首屏时间因素

image.png

在做性能优化的时候可以从根据优化侧重点选择合适的因素入手。

五、性能分析工具

网络耗时测定分段图

image.png

5.1 Network

1. 总览

image.png

2. 页面加载详情

image.png

  • DOMContentLoaded: DOM渲染完成时间
  • Load: 当前页面所有资源加载完成时间

3. 瀑布流

  • Queueing 浏览器将资源放入队列时间
  • Stalled 因放入队列时间而发生的停滞时间
  • DNS Lookup DNS解析时间
  • Initial connection 建立HTTP连接的时间
  • SSL 浏览器与服务器建立安全性连接的时间
  • TTFB 等待服务端返回数据的时间
  • Content Download 浏览器下载资源的时间

image.png

image.png

5.2 Lighthouse

image.png 根据chrome的一些策略自动对网站做一个质量评估,并且会给出一些优化的建议。

2020年初,新的性能指标在Lighthouse v6中发布。不再推荐使用“首次有效渲染(FMP)”,并且后续的Lighthouse版本中引入最大内容渲染时间(LCP)和总阻塞时间(TBT)这两个指标。

5.3 Performance

专业的分析

image.png 使用performance可以计算出的时间

const timing = performance.getEntriesByType('navigation')[0]
DNS 解析耗时: timing.domainLookupEnd – timing.domainLookupStart
(以下省略timing)
TCP 连接耗时: connectEnd - connectStart SSL 
安全连接耗时: connectEnd - secureConnectionStart 
网络请求耗时 (TTFB): responseStart - requestStart 
数据传输耗时: responseEnd - responseStart 
DOM 解析耗时: domInteractive - responseEnd 
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart 
白屏时间: responseEnd - fetchStart 
首次可交互时间: domInteractive - fetchStart 
DOM Ready 时间: domContentLoadedEventEnd
- fetchStart 
页面完全加载时间: loadEventStart - fetchStart
头部大小: transferSize - encodedBodySize 
重定向次数:performance.navigation.redirectCount 
重定向耗时: redirectEnd - redirectStart

5.4 资源打包分析

webpack-bundle-analyzer 配置好之后得到的打包信息

image.png

5.5 WebPageTest

可以模拟不同场景下访问的情况,比如模拟不同浏览器、不同国家等等,在线测试地址: www.webpagetest.org/

image.png

六、前端性能优化方法

6.1 常规优化-雅虎35条军规

image.png 一些解析:

  1. 提升网络性能:
  2. 目前主流浏览器一般限制同域名最大并发请求数为6,减少HTTP请求数的同时压缩请求文件的大小,意味着可以在最短时间内加载最多的内容,优先加载先使用到的资源。
  3. 使用缓存可以减少在网络传输和解析上消耗的时间。
  4. 不是每个前端应用都需要框架或相关依赖,减少不必要的依赖,可以有效缩小应用文件。
  5. 利用浏览器空闲时间预加载需要的资源。
  6. 提升运行时性能:
  7. 减少页面重绘(样式表)和回流(DOM树)。
  8. 使用响应式内容(例如媒体查询)。
  9. 懒加载策略,避免不必要资源的请求和处理影响当前性能。

6.2 具体去优化一个Vue工程

1.入口文件index.html

在此加入合适的骨架屏或仅仅一个简单的应用loading

image.png

2.按需引入依赖

如element的组件的按需引入,或语言包的按需引入

image.png

3.自定义字体加载时机

浏览器加载 Web Fonts 时按顺序会有三个时期:

  • 阻塞期(Block Period)。在此期间如果字体没有加载完成,那么浏览器会使用 font-family 指定的字体列表中的后备字体(Fallback)进行渲染,但是显示为空白,也就是对于用户是不可见的。在此期间字体加载完成之后才能正常显示该字体。
  • 交换期(Swap Period)。跟阻塞期类似,但是在这个时期内,它会在字体加载时,先用后备字体渲染文本并显示出来(而不是显示空白),在此期间字体加载完成之后才能正常的显示该字体。
  • 失败期(Failure Period)。如果字体加载失败,则使用后备字体显示文本。

至于每个时期有多长,是根据 font-display 属性的值来确定的。

直接进入交换期,避免字体未加载完成而出现的白屏。

/** chrome, firefox, opera,Safari, Android, i0s 4.2+*/
@font-face {
font-family: "微软雅黑";
src: url('~@/assets/fonts/微软雅黑.ttf'); 
font-display: swap;
}

基本的根元素样式


html,
body {
  width: 100%;
  height: 100%;
  font-size: $font-root; // 1rem
  font-family: 'Microsoft Yahei', Avenir, Helvetica, Arial, sans-serif;
  font-family: -apple-system, 'PingFang SC', 'Helvetica', 'Helvetica Neue',
    'Droidsansfallback', 'Droid Sans', 'Arial';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  ::-webkit-scrollbar {
    /*滚动条整体样式*/
    width: 8px;
    /*高宽分别对应横竖滚动条的尺寸*/
    height: 8px;
    cursor: pointer;
    background: transparent;
  }

  ::-webkit-scrollbar-thumb {
    /*滚动条里面小方块*/
    border-radius: 3px;
    background-color: $color-scrollbar;
  }

  ::-webkit-scrollbar-track {
    /*滚动条里面轨道*/
    border-radius: 3px;
    border: none;
  }
}

4.Webpack配置优化

image.png

image.png

5.路由懒加载

image.png

6.根据Vue的特性进行优化,例如:

  • v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 适用于需要非常频繁切换条件的场景。
  • 保证遍历数据的Key值唯一性能有效提升DOM树的DIFF计算效率
  • 充分利用各个钩子,配合Vue生命周期管理页面DOM、数据和事件,例如在beforeDestroyed中解绑不需要监听的事件方法
  • 使用函数式组件,如:

该组件会被渲染成一个普通的vnode而不是一个需要递归子组件的组件vnode,在vue中能够降低不必要的渲染开销

  • 拆分子组件,同第上所述,由于Vue会递归组件vnode的子组件,若子组件本身的依赖没有被触发更新,那么这个子组件将不会重新渲染
  • 将放在模板中的计算转移到computed中,由于computed具有缓存的特性,只要改计算属性的依赖不更新,就不会产生二次计算。
  • 在方法中将需要访问this.xxx的地方替换为方法的局部变量,将xxx解构出来作为参数,减少触发xxx的getter来降低开销
  • 使用keep-alive缓存组件,们切换路由视图,都会重新渲染一次组件,渲染组件就会经过组件初始化,render、patch 等过程,如果组件比较复杂,或者嵌套较深,那么整个渲染耗时就会很长

7.长列表性能优化

  • 数据懒加载
  • 已加载数据性能优化 在兼容性允许的条件下,除了常规的虚拟列表(每行高度固定以计算虚拟列表总高度),还可以使用IntersectionObserver来检测元素是否在视窗内,从而控制元素渲染,不受固定列表项高度限制。

8.数据缓存设计

  • 减小cookie
  • 缓存不常用的如数据字典、语言包数据
  • 缓存数据的更新通知机制,在数据发生变化时更新已缓存的数据,比如简单的使用接口中的某个字段标记通用数据字典是否需要更新。