如果让我来做性能优化【前端篇】

  • Post author:
  • Post category:其他




前言


更好的体验才会俘获用户的芳心

性能优化一直是Web端的”热度之王“,不论是日常工作还是面试,都是

重中之重

;小生不才,将我工作所做、书中所得呈现在各位看官面前,供各位看官品味个中滋味。



一、why?



1.性能是留住用户的关键

  • Pinterest 将感知等待时间减少了 40%,这将搜索引擎流量和注册量增加了 15% 。
  • COOK 将页面平均加载时间减少了 850 毫秒,从而将转化次数提高了 7%,将跳出率降低了 7%,并将每个会话的页面增加了 10% 。
  • BBC 发现他们的网站加载时间每增加一秒,他们就会失去 10% 的用户。
  • DoubleClick by Google 发现,如果网页加载时间超过 3 秒,则会有 53% 的用户放弃移动网站的访问。



2.性能意味着提高转化率

留住用户对于提高转化率至关重要。慢速网站对收入有负面影响,而快速网站显示可以提高转化率。

  • 对于 Mobify而言,主页加载速度每提高 100 毫秒,基于会话的转化率就会增加 1.11%,平均年收入增加近 380,000 美元。此外,结账页面加载速度每提高 100 毫秒,基于会话的转化率就会增加 1.55%,从而使年均收入增加近 530,000 美元。
  • 当 AutoAnything 将页面加载时间减少一半时,他们的销售额增长了 12% 到 13%。
  • 零售商 Furniture Village 审核了他们的网站速度,并制定了解决他们发现的问题的计划,导致页面加载时间降低了 20%,转化率提高了 10%。



3.性能关乎用户体验

当网站开始加载时,用户需要等待一定的时间出现内容。在此之前,没有用户体验可言。这种缺乏体验在快速连接上是短暂的。然而,在较慢的连接上,用户被迫等待。随着页面资源慢慢载入,用户可能会遇到更多问题。

非常慢的页面加载连接(顶部)与较快的页面加载连接(底部)比较



著名2-5-8原则

  • 当用户在2秒之内得到响应时,就会觉得系统的响应很快
  • 当用户在2~5秒之间得到响应时,就会觉得系统的响应速度还可以
  • 当用户在5~8秒以内得到响应时,就会觉得系统的响应速度很慢,但是还可以接受
  • 而当用户在超过8秒后仍然无法得到响应时,会感觉系统糟糕透了,或认为系统已经失去响应,而选择离开这个Web站点,或发起第二次请求。



4.影响性能的因素

从web服务本身看,一是客户端,二是服务端

  • 客户端

    对于客户端,用户使用不同的浏览器、不同的版本,可能对性能有不同程度的影响。抛开IE不谈,绝大多数情况下现代浏览器一般网页的性能差距不大。

    而用户网络方面,相较于10年前的3G网络或者4M宽带,到现在4/5G或者百M甚至千M宽带的普及,好了太多了。随着网络技术的发展,10年后可能我们现在做的所有网络层面的优化都将失去意义,但现阶段,我们还是需要为客户节省点儿流量。
  • 服务端

    我们就是服务方,也就是乙方。

    硬件层面,我们的服务也要受网卡、带宽及至运营商的限制。所在服务器的CPU、内存、磁盘、操作系统以及我们程序的开发语言、框架、软件选择,任何一个环节,都可能对服务性能产生影响。

    当然,这些多数是运营团队的职责。我们要做的,就是把自己力所能及的一摊子做到最好。



二、性能指标

不断优化用户体验是所有网站取得长远成功的关键。无论您是一名企业家、营销人员,还是开发者,Web 指标都能帮助您量化网站的体验指数,并发掘改进的机会。

指标 解释
FP 白屏(First Paint Time ) 从页面开始加载到浏览器中检测到渲染(任何渲染)时被触发(例如背景改变,样式应用等)
FCP 首屏(first contentful paint ) 从页面开始加载到页面内容的任何部分呈现在屏幕上的时间。 (关注的焦点是内容,这个度量可以知道用户什么时候收到有用的信息(文本,图像等))
FMP 首次有效绘制(First Meaningful Paint ) 表示页面的“主要内容”,开始出现在屏幕上的时间点,这项指标因页面逻辑而异,因此上不存在任何规范。(只是记录了加载体验的最开始。如果页面显示的是启动图片或者 loading 动画,这个时刻对用用户而言没有意义)
LCP(Largest Contentful Paint ) LCP 指标代表的是视窗最大可见图片或者文本块的渲染时间。 (可以帮助我们捕获更多的首次渲染之后的加载性能,但这项指标过于复杂,而且很难解释,也经常出错,没办法确定主要内容什么时候加载完。)
长任务(Long Task) 当一个任务执行时间超过 50ms 时消耗到的任务 (50ms 阈值是从 RAIL 模型总结出来的结论,这个是 google 研究用户感知得出的结论,类似用户的感知/耐心的阈值,超过这个阈值的任务,用户会感知到页面的卡顿)
TTI (Time To Internative) 从页面开始到它的主要子资源加载到能够快速地响应用户输入的时间。(没有耗时长任务)
首次输入延时 FID (first Input Delay) 从用户第一次与页面交互到浏览器实际能够开始处理事件的时间。(点击,输入,按键)
总阻塞时间 TBT(total blocking time ) 衡量从 FCP 到 TTI 之间主线程被阻塞时长的总和。
DCL (DOMContentLoaded) 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式,图像和子框架的完成加载。
L(onLoaded) 当依赖的资源,全部加载完毕之后才会触发
CLS(Cumulative Layout Shift) 是所有布局偏移分数的汇总,凡是在页面完整生命周期内预料之外的布局偏移都包括。布局偏移发生在任意时间,当一个可见元素改变了它的位置,从一个渲染帧到下一个

上面介绍了 11 种性能指标,我们没必要搞懂每一个指标的定义



1.核心 Web 指标

核心 Web 指标的构成指标会随着时间的推移而发展 。当前针对 2020 年的指标构成侧重于用户体验的三个方面——加载性能、交互性和视觉稳定性——并包括以下指标(及各指标相应的阈值):

关键性能指标


  • Largest Contentful Paint (LCP)

    :最大内容绘制,测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的2.5 秒内发生。

  • First Input Delay (FID)

    :首次输入延迟,测量交互性。为了提供良好的用户体验,页面的 FID 应为100 毫秒或更短。

  • Cumulative Layout Shift (CLS)

    :累积布局偏移,测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应保持在 0.1. 或更少。



2.查看指标



(1)web-vitals

通过使用web-vitals库,测量每项指标就像调用单个函数一样简单:

<script type="module">
const reportWebVitals = onPerfEntry => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(onPerfEntry);
      getFID(onPerfEntry);
      getFCP(onPerfEntry);
      getLCP(onPerfEntry);
      getTTFB(onPerfEntry);
    });
  }
};
export default reportWebVitals;

谷歌性能工具

或者使用谷歌扩展工具

web-vitals-extension



(2)Performance API

The Performance API is a group of standards used to measure the performance of web applications;

是一个浏览器全局对象,提供了一组 API 用于编程式地获取程序在某些节点的性能数据。它包含一组高精度时间定义,以及配套的相关方法。

window.performance

咱们如果想自定义搜集性能数据指标做前端的性能监控系统,那么使用

performance.mark

以及

performance.measure

这两个api是非常给力的。

这块具体的

代码示例

,建议大家可以直接访问

这里

去查看。



(3)Google performance 面板

概况图

在这里插入图片描述



(4)lighthouse

目前官方提供了

google devtools



google

插件、

npm cli

方式应用

lighthouse


点击生成报告:

lighthouse

就可得知当前网站的得分,以及chrome提出的优化建议。



三、针对性优化



1.资源优化



(1)使用 Brotli 进行纯文本压缩

2015 年,Google推出了Brotli,这是一种全新的开源无损数据格式,并被现在所有现代浏览器支持。

brotli


Brotli

有比

Gzip



Deflate

更高的压缩率,但是同时也需要更长的压缩时间,所以在请求的时候实时进行压缩并不是一个很好的办法。但我们可以预先对静态文件进行压缩,然后直接提供给客户端,这样我们就避免了

Brotli

压缩效率低的问题,同时使用这个方式,我们可以使用压缩质量最高的等级去压缩文件,最大程度的去减小文件的大小。

另外,由于不是所有浏览器都支持

Brotli

算法,所以在服务端我们需要同时提供两种文件,一个是经过

Brotli

压缩的文件,一个是原始文件,在浏览器不支持

Brotli

的情况下,我们可以使用

Gzip

去压缩原始文件提供给客户端。


Brotli

可用于任何纯文本的内容如

HTML



CSS



SVG



JavaScript

等。

使用最高压缩比配置的

Brotli + Gzip

预压缩静态资源,并使用

Brotli

配置 3~5 级压缩比来快速压缩

HTML

。确保服务器正确处理

Brotli



Gzip

的内容协商头。

compression



(2)图片优化

  • CDN

    有的

    CDN

    也提供图片尺寸的裁剪,根据不同的参数返回不同质量的图片,不过一般要收费。

  • 压缩

    早有优秀的工具可以进行压缩,分为有损压缩和无损压缩,对图片质量要求不高的场景可以考虑有损压缩,比如生成缩略图。

  • 格式


    WebP

    图片是一种新的图像格式,由 Google 开发。与

    png



    jpg

    相比,相同的视觉体验下,

    WebP

    图像的尺寸缩小了大约

    30%

    。另外,WebP图像格式还支持有损压缩、无损压缩、透明和动画。理论上完全可以替代

    png



    jpg



    gif

    等图片格式,不过目前

    WebP

    的还没有得到全面的支持,但是我们还是能够通过兜底方案来使用它。

    在这里插入图片描述

    所以在使用图片时尽可能使用具有

    srcset



    sizes

    和 元素的响应式图像。在使用它的同时,还可以通过 元素和

    JPEG

    兜底来使用

    WebP

    格式。

    假如使用了

    gif

    图片,可把它转换

    mp4



    WebM

    (从名字上能看出来跟WebP是一对,都是google推出的)。

  • 缓存


    图片大了

    ,更要缓存复用了。强缓存、协商缓存及至

    Service Worker

    ,对图片都是有效的。这里就不再赘述了。


    图片多了

    ,可以把图片放到不同的服务器请求,因为浏览器针对一模一样的资源(不只是图片),在同一时间只下载一个(http1.1之后是并行6个),这也算是浏览器层面的防抖了。


    顺便一提

    ,浏览器缓存资源是有大小限制的,

    chrome

    我记得是

    50M

    。假设你有这么牛逼的大文件,建议存储到

    indexedDB

    中。

  • 合并

    • 雪碧图:

      优点:体积小、减小请求数。

      缺点:1)丧失CSS部分灵活性。2)首屏如果不需要某个子图片,但这张大图里却包含了,那就属于资源浪费了。3)变更后缓存收益递减,改动一个子图片,整个图片都要重新生成,那这次的浏览器缓存就失效了。
    • 矢量图表库:

      大小颜色自由变换,按需使用。
    • base64:

      图片的base64编码就是可以将一张图片数据编码成一串字符串,使用该字符串代替图像地址url,一般对 4kb (有的是设置 10kb,根据自己项目情况衡量)以下的图片做 base64。
  • 加载

    • 按需加载

      回到网页本身,加载了10张图片,但页面上只显示了2张,剩下的图片需要滚动才能看到。那么,是不是意味着最少下面6张图片不是必须首屏加载的?世人可能就喜欢看蒙娜丽莎微笑,不喜欢看她的大粗腿呢?

在这里插入图片描述


按需加载

也是一个很重要的关键词,不只是图片领域,

js



css

这些资源无一例外。首屏不需要的资源和视窗之外的资源,尽可能

不要加载



你可能对这个样例体会不深,假设这里不是

10

张图片,而是

1000

张甚至

10000

张,你肯定就是另一番感受。

长列表

的滚动是个很

常见

的需求,可以想想怎么实现。

一定要把这

4

个字刻在你的骨子里。吃多少,拿多少,做新时代的好青年。

在这里插入图片描述

  • 延迟加载

    从某种意义上说,图片的

    按需加载

    也是

    延迟加载



    针对在

    CSS

    中的图片,如果没有用到这个

    class

    ,是不会加载的。可以通过改变

    class

    来达到延迟的目的。

    将图片的真实地址隐藏在

    ata-src

    属性里,在合适的时候再将它设置到

    src

    中。如果一开始显示是缩略图,再到后面替换成真实的图片,也是一样的处理逻辑。

    毫无疑问,

    延迟加载

    也是个很重要的

    关键词

    。计算机的哲学与人生类似,缓存就像经验的积累,而

    延迟

    则是以静致动、以慢打快的无上绝学。

  • 不同设备展示不同的图片

// 其中srcset指定图片的地址和对应的图片质量。sizes用来设置图片的尺寸零界点。
// 例子中的sizes就是指默认显示128px, 如果视区宽度大于360px, 则显示340px。
<img src = "image-128.png"
     srcset = "image-128.png 128w, image-256.png 256w, image-512.png 512w"
     sizes = "(max-width: 360px) 340px, 128px" 
/>



(3)字体优化

  • 字体大

    常见的字体类型有:

    EOT



    OTF



    TTF



    SVG



    WOFF



    WOFF2

    等。

    推荐

    WOFF2

    ,最小。缺点当然是浏览器兼容问题。

  • 字体多

    按需加载、延迟加载,首屏不需要的,就不要加载。

  • 闪动

    • 什么是字体闪动呢?

      就是你一段文字,要使用你的特定字体,但这时字体文件还没加载或加载完成,所以先显示的

      系统

      字体,直到你的字体加载完了,产生了变化。这个过程有人称闪动,有人称

      抖动

    • 原因

      通常字体文件是在

      CSS

      中使用的,浏览器先下载了

      CSS

      ,之后才知道有字体文件要下载,所以造成上面的

      现象

      。为了解决这个问题,就需要你告诉

      浏览器

      ,我的页面有个

      字体文件

      要下载,赶紧先下载,也就是把它的加载

      优先级

      提高。
    • 怎么做呢?还是上面的preload大法:


      <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>


      有一点需要指明,获取字体时必须加上

      crossorigin

      属性,就如使用

      CORS

      的匿名模式获取一样。是的,即使你的字体与页面同域。



2.构建优化



(1)关注dom节点

稍微写过几行

div

的同学都知道页面是基于

DOM树

的构建和

CSS树

的结合构成的

Render树

。然而很少有前端程序员在性能优化的时候讲到

dom节点的数量

。这里引用一种有关性能优化用一个简单但非完全准确的公式来表现这个过程的复杂度。

M:代表Dom节点的数量
N:代表Css节点的数量
Z:遍历循环的次数

// 遍历循环的次数
Z = M * N

随附一段计算页面节点的方法如下,可以用这段代码来检查页面中的node节点的数量:

function countNodes(node) {
    let count = 1;
    //  判断是否存在子节点
    if(node.hasChildNodes()) {
        //  获取子节点
        var cnodes = node.childNodes;
        //  对子节点进行递归统计
        for(var i=0,len=cnodes.length; i<len; i++) {
            count  += countNodes(cnodes.item(i))
        }
    }
    return count;
}
//  统计body的节点数量
countNodes(document.body)

DOM节点是我们每一个前端程序猿都要关注的!



(2)js、css资源位置

JS在

body

标签底部引入或书写,CSS在

header

标签里面引入或书写。

注意:移动端用于计算


rem


的 js 文件需要放于header头部,避免页面二次布局。



(3)构建体积

  • 小包替大包,手写替小包

    有些

    第三方包

    功能很强大很齐全,但我们只是用到里面的一个

    小功能

    而已,将这么大而全的包引入项目中有点

    得不偿失

    ,可以选择找能实现同样功能的小包或者手撕进行

    替换

    • 比如

      dayjs

      替换

      momentjs



      momentjs

      不做

      Brotli

      的话也接近

      100 kb

      了。
    • 比如页面用到了

      Promise

      ,没必要将整个

      babel-polyfill

      打进去,装个

      es6-promise

      就好了。
    • 不是吧!组长。一个

      防抖函数

      你让我引入

      lodash

  • 资源混淆压缩

    这个点是

    减少资源大小,该手段优化效果明显

    • 混淆压缩资源:一方面是

      降低代码可读性

      达到一定的

      安全作用

      ,一方面是

      减少资源的大小

    • 目前工程化实践:不用管,

      webpack

      构建自动安排好了。(手写配置的自己记得配上混淆)
  • 延迟加载第三方包

    可以减少

    首屏加载渲染

    的压力。

    问题:要用到的时候才去加载第三方包,就存在第三方包

    加载过长

    导致用户觉得卡顿的情况,这是它的

    劣势

    ,可以对资源做

    preload



    prefetch

    来解决这个问题。

    • 这个点是减少

      首屏加载

      的资源,但整体页面需要加载的资源没有减少,只是做了

      分段处理

      ,优化效果明显。
    • 屏幕能展示的东西就这么多,对于不在屏幕中的内容,且依赖第三方

      CDN 包

      的功能,又或者用户手动触发某个功能依赖第三方 CDN 包(一般页面渲染过程中不需要使用),可以

      延迟加载

      这个

      CDN

      包,没必要在

      html

      中直接引入,用动态

      script

      插入使用。
  • 对资源做 preload 和 prefetch

    对要用到的

    资源提前加载好

    ,等要请求的时候直接使用加载好的资源去

    执行

    ,这个优化点对优化首屏加载效果看情况,对

    后续的操作体验

    比较好。


    • preload

      提前加载当前页面需要用到的资源(

      不执行

      ),

      prefetch

      提前加载下个页面要用到的资源(

      不执行

      )。
    • 结合《

      延迟加载第三方包

      》这个优化点延迟加载第三方包的手段,为了提升用户的

      交互体验

      ,我们将被我们延迟加载的第三方包使用

      preload

      进行

      预加载

      ,这样用户交互的时候,直接执行已经

      提前加载好

      的资源即可。
    • 同时对于

      前后端分离

      的项目,使用了

      路由懒加载

      ,这时候加上

      prefetch

      提前加载下一个页面的资源,可以让用户

      无感切换

      页面,不会出现

      页面延迟

      出现的情况。
    • 目前工程化实践:vue 脚手架(3.0开始)默认都带着这个

      功能

      ,自己配的

      webpack

      配置加上

      @vue/preload-webpack-plugin

      插件。
    	// 示例:
    	<link href=/js/demo.js rel=prefetch>
     	<link href=/css/demo.css rel=preload as=style>
     	<link href=/js/demo.js rel=preload as=script>
    
  • 组件库按需加载

    • 这个点是针对前后端分离的前端应用,优化方向是

      减少资源大小

      ,优化效果明显。
    • 一个完整的组件库多大**40+**个组件,整个包的大小加上

      Brotli压缩

      至少也是

      100kb

      以上(

      element-ui 300kb+

      ),但我们项目中常用的组件也就十几二十个,不需要用到那么多组件。
    • 除了让多个系统最高效地复用静态资源缓存,会考虑对第三方包用

      CDN

      而非

      npm

      安装,但是具体情况得多系统衡量,单个系统的优化并不明显(

      需要灵活应变

      )。



(4)你正在使用 tree-shaking、scope hoisting 和 code-splitting 吗?


  • tree-shaking

    是一种清理构建包中无用依赖的方法,它让构建结果只包含生产中实际使用的代码,并消除

    Webpack

    中未使用的引入。借助

    Webpack



    Rollup

    ,我们还可以实现

    scope hoisting

    ,这两个工具都可以检测到

    mport

    链可以在哪个位置终止并转换为一个内联函数,而不破坏代码。借助

    Webpack

    ,我们还可以使用

    JSON Tree Shaking


  • code-spliting



    Webpack

    的另一个功能,可以把你的代码拆分为按需加载的

    chunk

    。并不是所有

    JavaScript

    都必须立即下载、解析和编译。一旦在代码中定义了分割点,

    Webpack

    就可以处理依赖关系和输出文件。它可以让浏览器保持较小的初始下载量,并在应用程序请求时按需请求代码。
  • 考虑使用

    preload-webpack-plugin

    ,这个插件可以根据你代码的分隔方式,让浏览器使用 或 对分隔的代码

    chunk

    进行预加载。

    Webpack

    内联指令还可以对

    preload/prefetch

    进行一些控制(但是请注意优先级问题。)



(5)识别并删除未使用的 CSS / JS


Chrome

中的

CSS



JavaScript

代码覆盖率工具(Coverage) 可以让我们了解哪些代码已执行或应用,哪些未执行。我们可以启动一个覆盖率检查,然后查看覆盖率结果。一旦检测到未使用的代码,找出那些模块并使用 import() 延迟加载。然后重复代码覆盖率检查确认现在在初始化时加载代码有变少。

你可以使用

Puppeteer

来收集代码覆盖率,

Puppeteer

还有许多其他用法,例如在每次构建时监视未使用的

CSS



此外,

purgecss



UnCSS



Helium

可以帮助你从

CSS

中删除未使用的样式。

识别并删除未使用的 CSS / JS



(6)能否将 JavaScript 抽离到 Web Worker?

为了缩短可交互时间的耗时,最好将有繁重计算的

JavaScript

抽离到

Web Worker

中或通过

Service Worker

进行缓存。因为

DOM

操作是与

JavaScript

一起运行在主线程上。使用

Web worker

可以将这些昂贵的操作转移到后台其他线程上运行。可以通过

Web Worker

预先加载和存储一些数据,以便后续在需要时使用它。可以使用

Comlink

来简化与

Web Worker

之间的通信。



(7)能否将频繁执行的功能抽离到 WebAssembly?

我们可以将繁重的计算任务抽离到

WebAssembly

(WASM)执行,它是一种二进制指令格式,被设计为一种用高级语言(

如 C / C ++ / Rust

)编译的可移植的对象。而且大多数现代浏览器都已经支持了 WebAssembly,并且随着

JavaScript



WASM

之间的函数调用变得越来越快,这个方式会变得越来越可行。

WebAssembly

的目的并不是替代

JavaScript

,而是可以在你发现当

CPU

占用过高时作为

JavaScript

的补充

JavaScript

更适合大多数

Web

应用程序,而

WebAssembly

最适合用于计算密集型

Web 应用程序

,例如

Web 游戏



(8)模块化

我们只想通过网络发送必要的

JavaScript

,但这意味着对这些资源的交付要更加

专注



细致



module/nomodule

的思想是编译并提供两个单独的

JavaScript

包:“常规”构建的构建方式是,一个包含

Babel

转换和

polyfills

,仅提供给实际需要它们的旧版浏览器,另一个包(相同功能)不包含

Babel

转换和

polyfills




JS module

(或者称作ES module,ECMAScript module)是一个主要的新特性,或者说是一系列新特性。你可能已经使用过第三方的模块加载系统。

CommonJs



NodeJs



AMD



RequireJs

等等。这些模块加载系统都有一个共同点:它们允许你执行导入导出操作。

能够认识

type=module

语法的浏览器会忽略具有

nomodule

属性的

script

。也就是说,我们可以使用一些脚本服务于支持

module

语法的浏览器,同时提供一个

nomodule

的脚本用于哪些不支持

module

语法的浏览器,作为补救。

浏览器支持

提示:使用

type=module

构建的文件体积优化相比常规构建的文件减少

30% ~ 50%

,而且还能期待下浏览器对新语法的性能优化。



(9)使用哪种前端页面渲染方案

使用

客户端渲染

还是

服务端渲染

?这都得由 “应用程序” 的性能来决定。

对于用户而言,

First Paint



First Meaningful Paint



TTI

这几个指标可以直接影响到用户体验。

最好的方法是设置某种

渐进式

引导:使用

服务端渲染

来快速获得第一个有意义的图形(

FCP

),同时包括一些

最小体积

的必需的

JavaScript

,尽量让可交互时间(

TTI

)紧挨着第一个有意义的图形的绘制。如果

JavaScript

执行在

FCP

之后太晚,浏览器会在解析、编译和执行后来执行的

JavaScript

时锁定主线程,从而削弱了网站或应用程序的交互性。

为了避免这种情况,我们务必将函数的执行分解为单独的

异步任务

,并尽可能使用

requestIdleCallback

。使用

WebPack

的动态

import()

支持,延迟加载部分 UI,避免在用户真正需要它们之前因为加载、解析和编译造成的成本消耗。

进入可交互状态后,我们可以按需或在时间允许的情况下启动应用程序的非必需部分。不过框架通常没有面向开发者提供简单的优先级概念,因此,对于大多数库和框架而言,实现逐步启动并不容易。

下面我们来分析下目前的几种

渲染机制

  • CSR(Client Side Rendering)

    CSR(Client Side Rendering)

    浏览器(Client)渲染顾名思义就是所有的

    页面渲染



    逻辑处理



    页面路由



    接口请求

    均是在浏览器中发生。其实,现代主流的

    前端框架

    均是这种渲染方式,这种渲染方式的好处在于实现了

    前后端架构分离

    ,利于前后端职责分离,并且能够首次渲染迅速

    有效减少白屏时间

    。同时,

    CSR

    可以通过在打包编译阶段进行

    预渲染

    或者

    骨架屏

    生成,可以进一步提升

    首次渲染



    用户体验



    但是由于和服务端会有多次交互(获取静态资源、获取数据),同时依赖浏览器进行渲染,在移动设备尤其是

    低配设备

    上,

    首屏时间



    完全可交互时间

    是比较长的。

  • SSR(Server Side Rendering)

    SSR(Server Side Rendering)

    服务端渲染则是在

    服务端

    完成页面的渲染,在服务端完成

    页面模板



    数据填充



    页面渲染

    ,然后将

    完整的HTML内容

    返回给到

    浏览器

    。由于

    所有的渲染工作

    都在服务端完成,因此网站的

    首屏时间



    TTI

    都会表现比较好。

    但是,渲染需要在

    服务端

    完成,并不能很好进行

    前后端职责分离

    ,而且

    白屏时间

    也会比较长,同时,对于服务端的

    负载要求

    也会比较高。


    • SSR



      CSR

      的页面渲染体验对比:

      **SSR**和**CSR**的页面渲染体验对比
  • 基于Hydration的SSR和CSR融合

    基于Hydration的SSR和CSR融合

注意

bundle.js

仍然是全量的

CSR

代码,这些代码执行完毕页面才真正可交互。因此,这种模式下,

FP

(First Paint) 虽然有所提升,但

TTI

(Time To Interactive) 可能会变慢,因为在客户端二次渲染完成之前,页面无法响应用户输入(被 JS 代码执行阻塞了)

对于二次渲染造成交互无法响应的问题,可能的优化方向是增量渲染(例如 React Fiber),以及渐进式渲染/部分渲染。


SSR



CSR

均有各自的优点和缺点,因此,业界提出

前后端渲染同构

的方案来整合

SSR



CSR



整个页面的

加载



刷新

是通过

服务端渲染

来实现,在渲染生成的

HTML

中内嵌

JavaScript



数据内容

。通过这样的实现,可以达到和

SSR

相同的

首屏时间

,并且基于

Hybration

,可以生成前端的

虚拟Dom

,避免前端触发

二次渲染

  • SSG(Static Site Generation)


    SSG

    也就是静态站点生成,为了

    减缓服务器

    压力,我们可以在

    构建时生成静态页面

    ,备注:

    Next.js

    生成的静态页面与普通的静态页面是不一样的,也是拥有

    SPA

    的能力,切换页面用户不会感受到整个页面在刷新。

  • 客户端预渲染



    服务端预渲染

    相似,但不是在服务器上

    动态渲染页面

    ,而是在

    构建时

    就将应用程序渲染为

    静态 HTML



    在构建过程中使用

    renderToStaticMarkup

    方法而不是

    renderToString

    方法,生成一个没有

    data-reactid

    之类属性的静态页面,这个页面的主

    JS

    和后续可能会用到的路由会做

    预加载

    。也就是说,当初

    打包时页面

    是怎么样,那么

    预渲染

    就是什么样。等到 JS

    下载

    并完成执行,如果页面上有

    数据更新

    ,那么页面会

    再次渲染

    。这时会造成一种

    数据延迟

    的错觉。

    结果是

    TTFB

    (第一字节到达时间) 和

    FCP

    时间变少,并且缩短了

    TTI



    FCP

    之间的间隔。如果预期内容会发生很大变化,那么就无法使用该方法。另外,必须提前知道所有

    URL

    才能生成所有页面。

    客户端预渲染

  • 三方同构渲染

    如果可以使用

    Service Worker

    ,三方同构渲染也可能派上用场。这个技术是指:利用流式服务器渲染初始页面,等

    Service Worker

    加载后,接管

    HTML

    的渲染工作。这样可以让

    缓存的组件



    模板

    保持最新,还可以启用像单页应用一样的导航用以在同一会话中预渲染新视图。当可以在

    服务器



    客户端页面



    Service Worker

    之间

    共享相同模板



    路由代码

    时,此方法最有效。

    三方同构渲染

三方同构渲染,在三个位置使用相同的代码渲染:在

服务器

上,在

DOM

中或在

service worker

中。

服务端渲染到客户端渲染的技术频谱:

服务端渲染到客户端渲染的技术频谱

至于如何选择, 这里也给出一些不成熟的建议:

1.对

SEO

要求不高,同时对操作需求比较多的项目,比如一些

后台管理系统

,建议使用

CSR

。因为只有在执行完

bundle

之后, 页面才能交互,单纯能看到元素,却不能交互,意义不大,而且

SSR

会带来额外的开发和维护成本。

2.如果页面无数据,或者是纯静态页面,建议使用

SSG

。 因为这是一种通过

预览打包

的方式构建页面,也不会增加服务器负担。

3.对

SEO

有比较大需求同时页面数据请求多的情况,建议使用

SSR



3.传输优化



(1)对 JavaScript 库进行了异步加载

看下哪些

JavaScript

引擎在你的用户群中占主导地位,然后探索对其进行优化的方法。例如,当针对

Blink

浏览器、

Node.js

运行时和

Electron

中使用的

V8

进行优化时,可以使用

脚本流

来处理整体脚本。


脚本流

优化了

JavaScript

文件的解析。以前版本的

Chrome

会用一种简单的方法,在开始解析脚本之前完整的下载脚本,但在下载完成前并没有充分利用

CPU

。从 41 版本开始,

Chrome

会在下载开始后立即在单独的线程上

解析异步



延迟脚本

。这意味着解析可以在下载完成后的几毫秒内完成,并使页面加载速度提高最高

10%

。这对于大型脚本和慢速网络连接特别有效。

下载开始后,脚本流允许

async



defer script

在单独的后台线程上进行解析,因此在某些情况下,页面加载时间最多可缩短

10%

。而且,在

header

中使用

script defer

,可以使浏览器更早的发现资源,然后在后台线程解析它。

警告:

Opera Mini

不支持脚本延迟,因此,如果你的主要用户是使用

Opera Mini



defer

则将被忽略,从而导致渲染被阻塞,直到脚本执行完毕。



(2)合理利用路由懒加载

这个点只针对

前后端分离

的前端应用,优化效果看情况,有时候可能形成反效果。


  • 资源合并

    一般针对不经常修改的

    第三方包

    ,对于

    业务 JS 代码

    一般不做合并。

  • 路由懒加载

    是将页面的JS代码拆分成一个一个的

    chunk

    包,只有加载页面的时候再去加载 JS 资源。
  • 用户经常访问的页面,不要用

    路由懒加载

    ,经常访问的页面主要是首页;对于用户经常访问的页面,因为用户肯定需要将页面的 chunk js 加载下来才可以访问页面,路由懒加载会导致先加载资源清单

    manifest

    文件,再去加载页面的

    chunk

    包,这样就形成了一个资源的串行请求,本来可以一次加载下来却

    非得拆分成2次而且还是串行的不是并行的

一个页面的

chunk

包其实不会很大,将它合进主

chunk

里也不会带来明显的请求耗时加大,反而请求次数多了带来的

网络延迟消耗

比较明显。

合理利用路由懒加载



(3)使用 IntersectionObserver 和图片懒加载

一般来说,我们应该把所有耗性能的组件都做延迟加载,比如大的

JavaScript



视频



iframe



小组件

和潜在的要加载的

图片

。例如:

Native lazy-loading

可以帮助我们延迟加载

图片



iframe


Native lazy-loading

就是浏览器的

img

标签和

iframe

标签支持原生懒加载特性,使用

loading = “lazy”

语法标记即可。

根据测试:需要在

img

标签中显形设置

width



height

才会支持

延迟加载



IntersectionObserver

延迟加载脚本的最有效方式是使用

Intersection Observer API

,这个

API

可以异步观察目标元素与祖先元素或文档的

viewport

之间交集的变化。我们需要创建一个

IntersectionObserver

对象,它接收一个回调函数和相应的参数,然后我们添加一个观察目标。如下:

// threshold = 1.0 意味着 target 元素完全出现在 root 选项指定的元素中可见时,回调函数将会被执行。
let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

let target = document.querySelector('#listItem');
observer.observe(target);

当目标变成可见或不可见时,回调函数就会执行,所以当它和

viewport

相交时,我们可以在元素变得可见之前执行一些操作。所以,我们可以通过

rootMargin

(围绕根的边距)和

threshold

(一个数字或一组数字,表示目标的可见性的百分比)对何时

调用观察者

的回调进行

细粒度控制



(4)渐进加载图片

我们可以通过在页面中使用

渐进式图片加载

将延迟加载效果提升到新的高度。与

Facebook



Pinterest



Medium

类似,我们可以先加载低质量甚至模糊的图片,然后随着页面继续加载,使用

LQIP

(低质量图片占位符)技术将它们替换为高质量的完整版本。

配合

懒加载

,我们可以使用现成的库:

lozad.js

// 实例
<img data-src="https://assets.imgix.net/unsplash/jellyfish.jpg?w=800&h=400&fit=crop&crop=entropy"
    	  src="https://assets.imgix.net/unsplash/jellyfish.jpgw=800&h=400&fit=crop&crop=entropy&px=16&blur=200&fm=webp"
>
<script>
    function init() {
        var imgDefer = document.getElementsByTagName('img');
        for (var i=0; i<imgDefer.length; i++) {
            if(imgDefer[i].getAttribute('data-src')) {
                imgDefer[i].setAttribute('src',imgDefer[i].getAttribute('data-src'));
            }
        }
    }
    window.onload = init;
</script>



(5)优化渲染性能

使用

CSS



will-change

通知浏览器哪些元素和属性将会改变。

will-change: auto
will-change: scroll-position
will-change: contents
will-change: transform
will-change: opacity
will-change: left, top

will-change: unset
will-change: initial
will-change: inherit


CSS

大部分样式是通过

CPU

来计算的,但

CSS

中也有一些

3D

的样式和动画的样式,计算这些样式会有很多重复且大量的计算任务,可以交给

GPU

来跑。

浏览器在处理下面的

CSS

的时候,会使用

GPU

渲染:


  • transform

  • opacity

  • filter

  • will-change

这里要注意的是

GPU

硬件加速是需要新建图层的,而把该元素移动到新图层是个耗时操作,界面可能会闪一下,所以最好提前做。

will-change

就是提前告诉浏览器在一开始就把元素放到新的图层,方便后面用

GPU

渲染的时候,不需要做图层的新建。



(5)避免回流和重绘

说道回流和重绘,我们先来回顾下浏览器的渲染流程:


  • 构建 DOM 树


    • HMTL

      词法语法分析,转成对应的

      AST

      树。

  • 样式计算

    • 格式化样式属性,例如:rem -> px、white -> #FFFFFF 等。
    • 计算每个节点样式属性:根据

      CSS

      选择器与

      DOM

      树共同构建

      render

      树。

  • 生成布局树

    • 这里去除一些

      dispy:none

      等隐藏样式的元素,因为它们不在

      render

      树中。

  • 建立图层树

    • 主要分为「显式合成」和「隐式合成」。

      • 当重绘时就只需要

        重绘当前图层


  • 生成绘制列表

    • 将图层树转换成绘制的

      指令列表


  • 生成图块和位图

    • 绘制列表交付给

      合成线程

      ,进行图层分块。
    • 渲染进程中专门维护了一个

      栅格化线程池

      ,专门负责把图块交由

      GPU

      渲染。

    • GPU

      渲染后将位图信息传递给合成线程,合成线程将位图信息在显示器显示。

  • 显示器显示内容


    显示器显示内容

    其中第四步

    建立图层树

    很重要,我们再着重的讲一下。浏览器从

    DOM

    树画质到屏幕图形上,需要做

    树结构到层结构

    的转化。这里介绍4个点:


    • 渲染对象(RenderObject)


      一个

      DOM

      节点对应了一个渲染对象,渲染对象维持着

      DOM

      树的树形结构。渲染对象知道怎么去绘制

      DOM

      节点的内容,它通过向一个绘图上下文(GraphicsContext)发出必要的绘制指令来绘制

      DOM

      节点。


    • 渲染层(RenderLayer)


      浏览器渲染时第一个构建的

      层模型

      ,位于同一个层级坐标空间的渲染对象都会被归并到同一个

      渲染层

      中,所以根据层叠上下文,不同层级坐标空间的的渲染对象将会形成多个渲染层,以此来体现它们之间的

      层叠关系

      。所以,对于满足形成层叠上下文条件的渲染对象,浏览器会自动为其创建新的渲染层。通常以下几种常见情况会让浏览器为其创建新的渲染层:


      • document 元素

      • position: relative | fixed | sticky | absolute

      • opacity < 1

      • will-change | fliter | mask | transform != none | overflow != visible

    • 图形层(GraphicsLayer)


      图形层是一个负责生成最终准备呈现出来的

      内容图形

      的层模型,它拥有一个

      图形上下文

      (GraphicsContext),图形上下文会负责输出该层的位图。存储在共享内存中的位图将作为纹理(可以把它想象成一个从主存储器移动到图像存储器的位图图像)上传到

      GPU

      ,最后由

      GPU

      将多个位图进行合成,然后绘制到屏幕上,此时,我们的页面也就展现到了屏幕上。

      所以

      图形层

      是一个重要的

      渲染载体和工具

      ,但它并不直接处理

      渲染层

      ,而是处理

      合成层


    • 合成层(CompositingLayer)



      满足某些特殊条件的渲染层

      ,会被浏览器自动提升为合成层。合成层拥有

      单独

      的图形层,而其他不是合成层的渲染层,则会和

      第一个拥有图形层的父层共用一个



      那么一个渲染层满足哪些特殊条件时,才能被提升为合成层呢?这里也列举一些常见情况:


      • 3D transforms

      • video、canvas、iframe

      • opacity 动画转换

      • position: fixed

      • will-change

      • animation



        transition

        设置了

        opacity



        transform



        fliter



        backdropfilter

上面提到满足一些

特殊条件

的渲染层最终会被浏览器提升了合成层,称为

显式合成

。除此之外,浏览器在合成阶段还存在一种

隐式合成

。下面我们通过举例来看下:

  • 假设,我们有两个

    absolute

    定位的

    div

    在屏幕上交叠了,根据

    z-index

    的关系,其中一个

    div

    就会”盖在“了另外一个上边。

    css层级

  • 这时候,如果我们给

    z-index: 3

    设置

    transform: translateZ(0)

    ,让浏览器将其提升为合成层。提升后

    z-index: 3

    这个合成层就会在

    document

    上方,那么按理来说

    z-index: 3

    就会在

    z-index: 5

    上面,我们设置的

    z-index

    就会出现

    交叠关系错乱

    的情况。

    css层级

  • 为了纠正这种错误的交叠顺序,浏览器必须让原本应该“盖在”上边的

    渲染层

    也同时提升为合成层。这称为

    隐式合成



    渲染层提升为合成层之后,会给我们带来不少好处:

    • 合成层的位图,会交由

      GPU

      合成,比

      CPU

      处理要快得多;
    • 当需要重绘时,只需要

      重绘本身

      ,不会影响到其他的层;
    • 元素提升为合成层后,

      transform



      opacity

      才不会触发重绘,如果不是合成层,则其依然会触发重绘。

当然了,任何东西滥用都是会有副作用,例如:

  • 绘制的图层必须传输到

    GPU

    ,这些层的数量和大小达到一定量级后,可能会导致传输非常慢,进而导致一些低端和中端设备上出现闪烁。

  • 隐式合成

    容易产生过量的合成层,每个合成层都占用额外的内存,形成层爆炸。占用

    GPU

    和大量的

    内存资源

    ,严重损耗页面性能。而内存是

    移动设备

    上的宝贵资源,过多使用内存可能会导致浏览器崩溃,让性能优化

    适得其反

OK,大概知道了

浏览器的渲染原理

后,我们来看下如何在实际中去

减少回流和重绘

  • 始终在

    图像上

    设置宽度和高度属性:浏览器会在默认情况下会分配框并保留空间,后续图片资源加载完成后不需要回流。

  • 避免多次修改

    :例如我们需要修改一个

    DOM



    height/width/margin

    三个属性,这时候我们可以通过

    cssText

    去修改,而不是通过

    dom.style.height

    去修改。

  • 批量修改 DOM

    :将

    DOM

    隐藏或者克隆出来修改后再替换,不过现在浏览器会用队列来存储多次修改,进行优化。 这个是适用范围已经不是那么广了。

  • 脱离文档流

    :对于一些类似动画之类的频繁变更的

    DOM

    可以使用绝对定位将其脱离文档流,避免父元素频繁回流。



(6)尝试重新组合你的 CSS 规则

根据

CSS and Network Performance

的研究,按照

媒体查询

条件把

CSS

文件进行拆分可能对我们的页面性能有一定提升。这样,浏览器会使用高优先级检索关键CSS,使用

低优先级

处理其他的所有内容。

// 我们把所有的css放在一个文件中
<link rel="stylesheet" href="all.css" />

我们把所有的css放在一个文件中,浏览器会这样处理他:

所有的css放在一个文件中,浏览器会这样处理他


// 当我们将其拆分成按 media 查询的时候
<link rel="stylesheet" href="all.css" media="all" />
<link rel="stylesheet" href="small.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="medium.css" media="(min-width: 64em)" />
<link rel="stylesheet" href="large.css" media="(min-width: 90em)" />
<link rel="stylesheet" href="extra-large.css" media="(min-width: 120em)" />
<link rel="stylesheet" href="print.css" media="print" />

当我们将其拆分成按 media 查询的时候,浏览器会这样:

将其拆分成按 media 查询的时候,浏览器会这样

避免在

CSS

文件中使用

@import

,因为它的工作原理,会影响浏览器的

并行下载

。不过目前我们更多使用的是

scss



less

他们会将

@import

的文件直接包含在

CSS

中,并不会产生额外的

HTTP

请求。

另外,不要将 < link rel=“stylesheet”> 放在

async

代码段之前。如果

JavaScript

脚本不依赖于样式,可以考虑将异步脚本置于样式之上。如果存在依赖,可以将

JavaScript

分成两部分,将它们分别放到

CSS

的两边来加载。

动态样式也可能会有很高的代价,虽然因为

React

的性能很好,所以通常只会发生在大量组合组件并行渲染时才会出现这种情况。根据

The unseen performance costs of modern CSS-in-JS libraries in React apps

的研究,在

production

模式开启时,通过

CSS-in-JS

创建的组件可能会比常规的

React

组件多花一倍的渲染时间。所以在应用

CSS-in-JS

时,可以采用以下方案来提升你的程序性能:


  • 不要过度的组合嵌套样式组件

    :这可以让

    React

    需要管理的组件

    更少

    ,可以更快的完成渲染工作

  • 优先使用“静态”组件

    :一些

    CSS-in-JS

    库会在你的

    CSS

    没有

    依赖主题



    props

    的情况下优化其执行。你的标签模板越是 「静态」,你的

    CSS-in-JS

    运行时就越有可能执行得更快。

  • 避免无效的React重新渲染

    :确保只在需要的时候才渲染,这样可以避免

    React



    CSS-in-JS

    库的运行时工作。

  • 零运行时的 CSS-in-JS 库是否能适用于你的项目

    :有时我们会选择在

    JS

    中编写

    CSS

    ,因为它确实提供了一些很好的

    开发者体验

    ,同时我们又不需要访问额外的

    JS API

    。如果你的应用程序不需要对主题的支持,也不需要使用大量复杂的

    CSS props

    ,那么零运行时的

    CSS-in-JS

    库可能是一个很好的选择。使用零运行时的库,你能从你的

    bundle

    文件中减少

    12KB

    ,因为大多数

    CSS-in-JS

    库的大小在

    10KB-15KB

    之间,而零运行时的库(如 linaria)

    小于1KB



4.网络优化



(1)资源放 CDN


CDN(Content Delivery Network)是指内容分发网络

,也称为内容传送网络。由于

CDN

是为加快网络访问速度而被优化的

网络覆盖层

,因此被形象地称为”网络加速器”。

你可以简单理解,你人在

北京

,访问的就是北京的

服务器节点

,人在

成都

,访问的就是成都的

服务器节点


CDN

最适合部署

静态资源

,最大限度地

减少

了互联网因为

地域、运营商的差异而带来的网络损耗

它还有

额外的优点

,你自己的服务可能得考虑下同时

十万个用户访问

会不会崩,用它就不用担心了,不用考虑

负载均衡

,不用考虑

高可用

,专业的人干专业的事。

资源放 CDN



(2)HTTP缓存

再试想这样一个场景,假设你的网页万年不变,那是不是

用户

除首次外,接下来的每一次对你

服务器

的访问都是浪费呢?

针对这种情况,相信你也很容易想出

解决方案

,也就是我们下一个关键词——

HTTP缓存


HTTP

缓存分为以下两种,两者都是通过

HTTP 响应头

控制缓存。



强制缓存

再次请求时无需再向服务器发送请求

               client         server
GET /a.ab389z.js ------->
                      <------- 200 OK
(再也不会发请求)

与之相关的 Response Headers 有以下两个:

  • Expires

    这个头部很严格:使用绝对时间,且有固定的格式。

      Expires: Mon, 25 Oct 2021 20:11:12 GMT
    
  • Cache-Control,具有强大的缓存控制能力。

    常用的有以下两个:

    • no-cache,每次请求需要校验服务器资源的新鲜度。
    • max-age=31536000,浏览器在一年内都不需要向服务器请求资源。



协商缓存

再次请求时,需要向服务器校验新鲜度,如果资源是新鲜的,返回 304,从浏览器获取资源

           client         server
GET /a.js   ----------->
                   <----------- 200 OK
GET /a.js   ----------->
                   <----------- 304 Not Modified

与之相关的 Request/Response Headers 有以两个:

  • Last-Modified/If-Modified-Since,匹配 Response Header 的 Last-Modified 与 Request 的 If-Modified-Since 是否一致。
  • Etag/If-None-Match,匹配 Response Header 的 Etag 与 Request 的 If-None-Match 是否一致。



(3)ServiceWorker

现代浏览器除了

强缓存



协商缓存

外,还额外提供了一些

API

可以让你订制控制缓存。

我们熟知的浏览器的存储有哪些呢?最早的

Cookie

,到后面的

LocalStorage



SessionStorage

,再到浏览器的数据库

IndexedDB

。直接用它们可以实现对缓存的部分控制,但你没办法拦截网络的加载,比如页面都还没加载,你的代码都没工作,怎么拦截这个JS或图片说

不再加载

了?



ServiceWorker

(简称sw),就是应运而生的一个高级缓存控制器。

一句话描述的话,它就是

浏览器提供的代理

。网页的所有

网络请求

,都经它”中转”,这角色像不像

曹公公



曹公公插图

它是基于

web worker

的,可以访问

cache



indexedDB

sw 是基于

HTTPS

的,因为

Service Worker

中涉及到请求拦截,所以必须使用

HTTPS

协议来保障安全。如果是本地调试的话,

localhost

是可以的。

一般来说,它可以有效提升用户的

弱网体验

,移动端网页用的多些。

使用上也简单,只是维护起来并不容易,一般需要

框架



第三方库

来管理。

// html
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function () {
   	navigator.serviceWorker.register('./serviceWorker.js', { scope: './demo.html' })
      .then(function (registration) {
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    })
      .catch(function (err) {
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}
// serviceWorker.js
/* 监听安装事件,install 事件一般是被用来设置你的浏览器的离线缓存逻辑 */
this.addEventListener('install', function (event) {
    /* 通过这个方法可以防止缓存未完成,就关闭serviceWorker */
    event.waitUntil(
        /* 创建一个名叫V1的缓存版本 */
        caches.open('v1').then(function (cache) {
            /* 指定要缓存的内容,地址为相对于跟域名的访问路径 */
            return cache.addAll([
                './demo.html',
                './demo.js',
                './demo.css',
                './demo2.js',
                '/images/demo.jpg',
            ]);
        })
    );
});

/* 注册fetch事件,拦截全站的请求 */
this.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request).then(function (response) {
            return response || fetch(event.request);
        })
    );
});
  • 页面刷新一次后,在浏览器的

    Application

    里能看到

    Service Workers

    多了一条:

    Service Workers
  • 而网络里

    Size

    多了个标志:

    Service Workers


  • Timing

    里也多了:

    Timing

注意:如果你只是做测试,

sw

开启后最好到

Application

里把它注销掉,否则说不定会影响你的开发(如果你注册的端口号和网页刚好与现在的一样了)。



(4)启用 OCSP stapling

在服务器上启用

OCSP stapling

功能,可实现由全站加速预先缓存在线证书验证结果并下发给客户端,无需浏览器直接向

CA站点

查询证书状态,从而减少用户验证时间。



(5)HTTP协议优化

随着

HTTPS



HTTP/2

的流行,很多

HTTP/1.1

时代的优化策略已经不奏效了,甚至还有反优化的作用。

这里我们顺带把各个版本的

HTTP

协议做一下简单的分析:

  • HTTP/1.0

    • 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个

      TCP

      连接(

      TCP连接的新建成本很高,因为需要客户端和服务器三次握手

      ),服务器完成请求处理后立即断开

      TCP

      连接,服务器不跟踪每个客户也不记录过去的请求。
  • HTTP/1.1


    • 管道化(Pipelining)

      :提出管道化方案解决连接延迟,服务端可设置

      Keep-Alive

      来让连接延迟关闭时间,但因为浏览器自身的

      Max-Connection

      最大连接限制,同一个域名下的请求连接限制(同域下谷歌浏览器是一次限制最多6个连接),只能通过多开域名来实现,这也就是我们的静态资源选择放到

      CDN

      上或其它域名下,来提高资源加载速度。管道化方案需要前后端支持,但绝大部分的

      HTTP代理器

      对管道化的支持并不友好。

    • 只支持GET/HEAD

      :管道化只支持

      GET / HEAD

      方式传送数据,不支持

      POST

      等其它方式传输。

    • 头部信息冗余



      HTTP

      是无状态的,客户端/服务端只能通过

      HEAD

      的数据维护获取状态信息,这样就造成每次连接请求时都会携带大量冗余的头部信息,头部信息包括

      COOKIE

      信息等。

    • 超文本协议



      HTTP/1.X

      是超文本协议传输。超文本协议传输,发送请求时会找出数据的

      开头和结尾帧

      的位置,并去除多余空格,选择最优方式传输。如果使用了

      HTTPS

      ,那么还会对数据进行

      加密处理

      ,一定程度上会造成

      传输速度

      上的损耗。

    • 队头阻塞

      : 管道化通过延迟连接关闭的方案,虽然可同时发起对

      服务端

      的多个请求,但服务端的

      response

      依旧遵循

      FIFO

      (先进先出)规则依次返回。举个例子客户端发送了1、2、3、4四个请求,如果1没返回给客户端,那么2,3,4也不会返回。这就是所谓的「队头阻塞」。

      高并发高延迟

      的场景下阻塞明显。
  • HTTP/2.0


    • 多路复用

      :一个域只要一个

      TCP

      连接,实现真正的并发请求,降低延时,提高了带宽的利用率。


    • 头部压缩

      :客户端/服务端进行渐进更新维护,采用

      HPACK

      压缩,节省了报文头占用流量。

      1.相同的头部信息不会通过请求发送,延用之前请求携带的头部信息。

      2.新增/修改的头部信息会被加入到

      HEAD

      中,两端渐进更新。


    • 请求优先级

      :每个流都有自己的优先级别,客户端可指定优先级。并可以做流量控制。


    • 服务端推送

      :例如我们加载

      index.html

      , 我们可能还需要

      index.js

      ,

      index.css

      等文件。传统的请求只有当拿到

      index.html

      ,解析

      html

      中对

      index.js/index.css

      的引入才会再请求资源加载,但是通过服务端数据,可以提前将资源推送给客户端,这样客户端要用到的时候直接调用即可,不用再发送请求。


    • 二进制协议

      :采用

      二进制协议

      ,区别 与

      HTTP/1.X

      的 超文本协议。客户(服务)端发送(接收)数据时,会将

      数据打散乱序

      发送,接收数据时接收一端再通过

      streamID

      标识来将数据合并。二进制协议解析起来更高效、“线上”更紧凑,更重要的是错误更少。

      这里再补充一下

      HTTP2

      相对于

      HTTP1.1

      并不全是优点:因为

      HTTP2

      将多个

      HTTP

      流放在同一个

      TCP

      连接中,遵循同一个流量状态控制。只要第一个

      HTTP

      流遇到阻塞,那么后面的

      HTTP

      流压根没办法发出去,这就是「行头阻塞」。

  • HTTP/3.0

    采用

    QUIC

    协议,基于

    UDP

    协议,避免了

    TCP

    协议的一些缺点,采用

    TLS1.3



    HTTPS

    所需的

    RTT

    降至最少为0。

    • TCP 协议的不足


      • TCP

        可能会间歇性地挂起数据传输:如果一个序列号较低的

        数据段

        还没有接收到,即使其他序列号较高的段已经接收到,TCP 的接收机滑动窗口也不会继续处理。这将导致TCP 流瞬间挂起,在更糟糕的情况下,即使所有的段中有一个没有收到,也会导致关闭连接。这个问题被称为

        TCP

        流的行头阻塞(HoL)。

        TCP间歇性地挂起数据传输

      • TCP 不支持流级复

        :虽然

        TCP

        确实允许在

        应用层

        之间建立多个逻辑连接,但它不允许在一个

        TCP

        流中复用数据包。使用

        HTTP/2

        时,浏览器只能与服务器打开一个

        TCP

        连接,并使用同一个连接来请求多个对象,如

        CSS



        JavaScript

        等文件。在接收这些对象的同时,

        TCP

        会将所有对象序列化在同一个流中。因此,它不知道

        TCP

        段的对象级分区。

      • TCP 会产生冗余通信



        TCP

        连接握手会有冗余的消息交换序列,即使是与已知主机建立的连接也是如此。

        TCP 会产生冗余通信
    • QUIC 协议的优势


      • 选择UDP作为底层传输层协议

        :在

        TCP

        之上建立新的传输机制,将继承

        TCP

        的上述所有缺点。因此,

        UDP

        是一个明智的选择。此外,

        QUIC

        是在用户层构建的,所以不需要每次协议升级时进行内核修改。

      • 流复用和流控



        QUIC

        引入了连接上的多路流复用的概念。

        QUIC

        通过设计实现了单独的、针对每个流的流控,解决了整个连接的行头阻塞问题。

      • 灵活的拥塞控制机制



        TCP

        的拥塞控制机制是刚性的。该协议每次检测到拥塞时,都会将拥塞窗口大小减少一半。相比之下,

        QUIC

        的拥塞控制设计得更加灵活,可以更有效地利用可用的网络带宽,从而获得更好的

        吞吐量


      • 更好的错误处理能力



        QUIC

        使用增强的丢失

        恢复机制



        转发纠错

        功能,以更好地处理错误数据包。该功能对于那些只能通过缓慢的无线网络访问互联网的用户来说是一个福音,因为这些网络用户在传输过程中经常出现

        高错误率


      • 更快的握手



        QUIC

        使用相同的

        TLS

        模块进行安全连接。然而,与

        TCP

        不同的是,

        QUIC

        的握手机制经过优化,避免了每次两个已知的对等者之间

        建立通信时

        的冗余协议交换。

        QUIC 协议的优势

这里大概给两条公式看下

HTTP/3

在结合

HTTPS

下跟

HTTP/2

的对比,给大家一个比较直观的感受,具体细节不再简述。


HTTP/2下



HTTPS

通信时间总和 =

TCP

连接时间 +

TLS

连接时间 +

HTTP

交易时间 =

1.5 RTT + 1.5 RTT + 1RTT = 4 RTT




HTTP/3下

:首次链接时,

QUIC 采用 TLS1.3

,需要

1RTT

,一次

HTTP

数据请求,共

2RTT

。重连时直接使用

Session ID

,不需要再次进行

TLS

验证,所以只需要

1RTT

OK,大概了解的

HTTP

协议的版本特点后,我们来看在目前主流

HTTP2 + HTTPS

的时代下,哪些优化策略已经过时了甚至是反优化呢?


减少请求数



HTTP/1.1

因为存在「队头阻塞」,所以我们通常会采用合并资源,捆绑文件(雪碧图等)等方式来减少请求数。但在

HTTP/2

中我们更需要注重网站的缓存调优,传输轻量、细粒度的资源,方便独立缓存和并行传输。


多域名存储



HTTP/1.1

因为浏览器有最大连接数限制,所以我们会将资源分发到不同的域名下存放以此来增大最大连接数。但在

HTTP/2

中一个域只有一个链接,所以我们不需要去分多个域名存储,多域名存储甚至还会造成额外的

TLS

消耗。



(6)减小请求头的大小

减小

请求头

的大小,常见的情况是

Cookie

。例如我们的主站中(如:www.test.com ) 存储了很多的

Cookie

,我们的

CDN

域名(cdn.test.com)与我们主域一样,此时我们去请求时会附带上

.test.com

域下的

Cookie

。而且这些

Cookie

对于

CDN

毫无用处,会增大我们请求的包大小。所以我们可以将

CDN

域名与主域区分开,例如:淘宝(https://www.taobao.com)的

CDN

域名为 https://img.alicdn.com。



总结

感谢屏幕前的你看到了这里!

我从

为什么要进行性能优化

=>

查看性能指标

=>

罗列 Web 优化的一些方法

三个方面完成了本篇文章,随着

Web3.0

时代的到来,性能优化已经成为我们开发过程中的

重中之重



有了这些

优化的点

,相信你在

写代码

或者

优化老项目时

都能

游刃有余

,能提前

考虑到其中的一些坑,并且规避




注意:“鞋合不合适只有脚知道”,我们在对项目进行优化之前,一定要问自己或者团队一个问题 —— 是否合适?



相关资料


为什么速度很重要?



最全的前端性能定位总结



web性能优化



CSR、SSR、NSR、ESR傻傻分不清楚,一文帮你理清前端渲染方案!



什么是强缓存和协商缓存



你会怎么做前端优化?

水平有限,还不能写到尽善尽美,希望大家多多交流,跟春野一同进步!!!



版权声明:本文为jinchunye原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。