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

前言

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

性能优化一直是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 devtoolsgoogle插件、npm cli方式应用
lighthouse


点击生成报告:
lighthouse

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

三、针对性优化

1.资源优化

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

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

brotli
Brotli 有比 GzipDeflate 更高的压缩率,但是同时也需要更长的压缩时间,所以在请求的时候实时进行压缩并不是一个很好的办法。但我们可以预先对静态文件进行压缩,然后直接提供给客户端,这样我们就避免了 Brotli 压缩效率低的问题,同时使用这个方式,我们可以使用压缩质量最高的等级去压缩文件,最大程度的去减小文件的大小。

另外,由于不是所有浏览器都支持 Brotli 算法,所以在服务端我们需要同时提供两种文件,一个是经过 Brotli 压缩的文件,一个是原始文件,在浏览器不支持 Brotli 的情况下,我们可以使用 Gzip 去压缩原始文件提供给客户端。

Brotli可用于任何纯文本的内容如 HTMLCSSSVGJavaScript 等。

使用最高压缩比配置的 Brotli + Gzip 预压缩静态资源,并使用 Brotli 配置 3~5 级压缩比来快速压缩 HTML。确保服务器正确处理 BrotliGzip 的内容协商头。

compression

(2)图片优化

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

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

  • 格式
    WebP图片是一种新的图像格式,由 Google 开发。与 pngjpg 相比,相同的视觉体验下,WebP 图像的尺寸缩小了大约30%。另外,WebP图像格式还支持有损压缩、无损压缩、透明和动画。理论上完全可以替代pngjpggif等图片格式,不过目前WebP的还没有得到全面的支持,但是我们还是能够通过兜底方案来使用它。
    在这里插入图片描述
    所以在使用图片时尽可能使用具有 srcsetsizes 和 元素的响应式图像。在使用它的同时,还可以通过 元素和 JPEG 兜底来使用 WebP 格式。
    假如使用了 gif 图片,可把它转换 mp4WebM(从名字上能看出来跟WebP是一对,都是google推出的)。

  • 缓存
    图片大了,更要缓存复用了。强缓存、协商缓存及至 Service Worker ,对图片都是有效的。这里就不再赘述了。
    图片多了,可以把图片放到不同的服务器请求,因为浏览器针对一模一样的资源(不只是图片),在同一时间只下载一个(http1.1之后是并行6个),这也算是浏览器层面的防抖了。
    顺便一提,浏览器缓存资源是有大小限制的,chrome我记得是 50M。假设你有这么牛逼的大文件,建议存储到 indexedDB 中。

  • 合并

    • 雪碧图:
      优点:体积小、减小请求数。
      缺点:1)丧失CSS部分灵活性。2)首屏如果不需要某个子图片,但这张大图里却包含了,那就属于资源浪费了。3)变更后缓存收益递减,改动一个子图片,整个图片都要重新生成,那这次的浏览器缓存就失效了。
    • 矢量图表库:
      大小颜色自由变换,按需使用。
    • base64:
      图片的base64编码就是可以将一张图片数据编码成一串字符串,使用该字符串代替图像地址url,一般对 4kb (有的是设置 10kb,根据自己项目情况衡量)以下的图片做 base64。
  • 加载

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

在这里插入图片描述
按需加载也是一个很重要的关键词,不只是图片领域,jscss这些资源无一例外。首屏不需要的资源和视窗之外的资源,尽可能不要加载
你可能对这个样例体会不深,假设这里不是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)字体优化

  • 字体大
    常见的字体类型有:EOTOTFTTFSVGWOFFWOFF2等。
    推荐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 替换 momentjsmomentjs 不做 Brotli 的话也接近 100 kb了。
    • 比如页面用到了 Promise,没必要将整个 babel-polyfill 打进去,装个 es6-promise就好了。
    • 不是吧!组长。一个防抖函数你让我引入lodash
  • 资源混淆压缩
    这个点是减少资源大小,该手段优化效果明显
    • 混淆压缩资源:一方面是降低代码可读性达到一定的安全作用,一方面是减少资源的大小
    • 目前工程化实践:不用管,webpack构建自动安排好了。(手写配置的自己记得配上混淆)
  • 延迟加载第三方包
    可以减少首屏加载渲染的压力。
    问题:要用到的时候才去加载第三方包,就存在第三方包加载过长导致用户觉得卡顿的情况,这是它的劣势,可以对资源做 preloadprefetch 来解决这个问题。
    • 这个点是减少首屏加载的资源,但整体页面需要加载的资源没有减少,只是做了分段处理,优化效果明显。
    • 屏幕能展示的东西就这么多,对于不在屏幕中的内容,且依赖第三方 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中未使用的引入。借助 WebpackRollup,我们还可以实现 scope hoisting ,这两个工具都可以检测到 mport 链可以在哪个位置终止并转换为一个内联函数,而不破坏代码。借助 Webpack,我们还可以使用 JSON Tree Shaking
  • code-splitingWebpack 的另一个功能,可以把你的代码拆分为按需加载的chunk。并不是所有 JavaScript 都必须立即下载、解析和编译。一旦在代码中定义了分割点,Webpack 就可以处理依赖关系和输出文件。它可以让浏览器保持较小的初始下载量,并在应用程序请求时按需请求代码。
  • 考虑使用 preload-webpack-plugin,这个插件可以根据你代码的分隔方式,让浏览器使用 或 对分隔的代码chunk进行预加载。Webpack 内联指令还可以对 preload/prefetch 进行一些控制(但是请注意优先级问题。)

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

Chrome 中的 CSSJavaScript 代码覆盖率工具(Coverage) 可以让我们了解哪些代码已执行或应用,哪些未执行。我们可以启动一个覆盖率检查,然后查看覆盖率结果。一旦检测到未使用的代码,找出那些模块并使用 import() 延迟加载。然后重复代码覆盖率检查确认现在在初始化时加载代码有变少。
你可以使用 Puppeteer 来收集代码覆盖率,Puppeteer 还有许多其他用法,例如在每次构建时监视未使用的 CSS
此外,purgecssUnCSSHelium 可以帮助你从 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,并且随着 JavaScriptWASM 之间的函数调用变得越来越快,这个方式会变得越来越可行。WebAssembly 的目的并不是替代 JavaScript,而是可以在你发现当 CPU 占用过高时作为 JavaScript 的补充JavaScript 更适合大多数 Web 应用程序,而 WebAssembly 最适合用于计算密集型 Web 应用程序,例如 Web 游戏

(8)模块化

我们只想通过网络发送必要的 JavaScript,但这意味着对这些资源的交付要更加专注细致module/nomodule 的思想是编译并提供两个单独的 JavaScript 包:“常规”构建的构建方式是,一个包含 Babel 转换和 polyfills,仅提供给实际需要它们的旧版浏览器,另一个包(相同功能)不包含 Babel 转换和 polyfills
JS module(或者称作ES module,ECMAScript module)是一个主要的新特性,或者说是一系列新特性。你可能已经使用过第三方的模块加载系统。CommonJsNodeJsAMDRequireJs 等等。这些模块加载系统都有一个共同点:它们允许你执行导入导出操作。
能够认识 type=module 语法的浏览器会忽略具有 nomodule 属性的 script。也就是说,我们可以使用一些脚本服务于支持 module 语法的浏览器,同时提供一个 nomodule 的脚本用于哪些不支持 module 语法的浏览器,作为补救。
浏览器支持

提示:使用 type=module 构建的文件体积优化相比常规构建的文件减少 30% ~ 50%,而且还能期待下浏览器对新语法的性能优化。

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

使用 客户端渲染 还是 服务端渲染?这都得由 “应用程序” 的性能来决定。

对于用户而言,First PaintFirst Meaningful PaintTTI 这几个指标可以直接影响到用户体验。

最好的方法是设置某种渐进式引导:使用服务端渲染来快速获得第一个有意义的图形(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都会表现比较好。
    但是,渲染需要在服务端完成,并不能很好进行前后端职责分离,而且白屏时间也会比较长,同时,对于服务端的负载要求也会比较高。

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

注意 bundle.js 仍然是全量的 CSR 代码,这些代码执行完毕页面才真正可交互。因此,这种模式下,FP(First Paint) 虽然有所提升,但 TTI(Time To Interactive) 可能会变慢,因为在客户端二次渲染完成之前,页面无法响应用户输入(被 JS 代码执行阻塞了)
对于二次渲染造成交互无法响应的问题,可能的优化方向是增量渲染(例如 React Fiber),以及渐进式渲染/部分渲染。

SSRCSR均有各自的优点和缺点,因此,业界提出前后端渲染同构的方案来整合SSRCSR
整个页面的加载刷新是通过服务端渲染来实现,在渲染生成的HTML中内嵌JavaScript数据内容。通过这样的实现,可以达到和SSR相同的首屏时间,并且基于Hybration,可以生成前端的虚拟Dom,避免前端触发二次渲染

  • SSG(Static Site Generation)
    SSG 也就是静态站点生成,为了减缓服务器压力,我们可以在构建时生成静态页面,备注:Next.js 生成的静态页面与普通的静态页面是不一样的,也是拥有 SPA 的能力,切换页面用户不会感受到整个页面在刷新。

  • 客户端预渲染
    服务端预渲染相似,但不是在服务器上动态渲染页面,而是在构建时就将应用程序渲染为静态 HTML
    在构建过程中使用 renderToStaticMarkup 方法而不是 renderToString 方法,生成一个没有 data-reactid 之类属性的静态页面,这个页面的主 JS 和后续可能会用到的路由会做预加载。也就是说,当初打包时页面是怎么样,那么预渲染就是什么样。等到 JS 下载并完成执行,如果页面上有数据更新,那么页面会再次渲染。这时会造成一种数据延迟的错觉。
    结果是 TTFB(第一字节到达时间) 和 FCP 时间变少,并且缩短了 TTIFCP 之间的间隔。如果预期内容会发生很大变化,那么就无法使用该方法。另外,必须提前知道所有 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%。这对于大型脚本和慢速网络连接特别有效。

下载开始后,脚本流允许 asyncdefer script 在单独的后台线程上进行解析,因此在某些情况下,页面加载时间最多可缩短 10%。而且,在 header 中使用 script defer,可以使浏览器更早的发现资源,然后在后台线程解析它。

警告:Opera Mini 不支持脚本延迟,因此,如果你的主要用户是使用 Opera Minidefer 则将被忽略,从而导致渲染被阻塞,直到脚本执行完毕。

(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 标签中显形设置 widthheight 才会支持延迟加载
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)渐进加载图片

我们可以通过在页面中使用渐进式图片加载将延迟加载效果提升到新的高度。与 FacebookPinterestMedium 类似,我们可以先加载低质量甚至模糊的图片,然后随着页面继续加载,使用 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)优化渲染性能

使用 CSSwill-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
      • animationtransition 设置了opacitytransformfliterbackdropfilter

上面提到满足一些特殊条件的渲染层最终会被浏览器提升了合成层,称为显式合成。除此之外,浏览器在合成阶段还存在一种隐式合成。下面我们通过举例来看下:

  • 假设,我们有两个 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 处理要快得多;
    • 当需要重绘时,只需要重绘本身,不会影响到其他的层;
    • 元素提升为合成层后,transformopacity 才不会触发重绘,如果不是合成层,则其依然会触发重绘。

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

  • 绘制的图层必须传输到 GPU,这些层的数量和大小达到一定量级后,可能会导致传输非常慢,进而导致一些低端和中端设备上出现闪烁。
  • 隐式合成容易产生过量的合成层,每个合成层都占用额外的内存,形成层爆炸。占用 GPU 和大量的内存资源,严重损耗页面性能。而内存是移动设备上的宝贵资源,过多使用内存可能会导致浏览器崩溃,让性能优化适得其反

OK,大概知道了浏览器的渲染原理后,我们来看下如何在实际中去减少回流和重绘

  • 始终在图像上设置宽度和高度属性:浏览器会在默认情况下会分配框并保留空间,后续图片资源加载完成后不需要回流。
  • 避免多次修改:例如我们需要修改一个 DOMheight/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,因为它的工作原理,会影响浏览器的并行下载。不过目前我们更多使用的是 scssless他们会将 @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重新渲染:确保只在需要的时候才渲染,这样可以避免 ReactCSS-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,到后面的LocalStorageSessionStorage,再到浏览器的数据库IndexedDB。直接用它们可以实现对缓存的部分控制,但你没办法拦截网络的加载,比如页面都还没加载,你的代码都没工作,怎么拦截这个JS或图片说不再加载了?

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

一句话描述的话,它就是浏览器提供的代理。网页的所有网络请求,都经它"中转",这角色像不像曹公公
曹公公插图
它是基于web worker的,可以访问cacheindexedDB

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协议优化

随着 HTTPSHTTP/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.3HTTPS 所需的 RTT 降至最少为0。

    • TCP 协议的不足
      • TCP 可能会间歇性地挂起数据传输:如果一个序列号较低的数据段还没有接收到,即使其他序列号较高的段已经接收到,TCP 的接收机滑动窗口也不会继续处理。这将导致TCP 流瞬间挂起,在更糟糕的情况下,即使所有的段中有一个没有收到,也会导致关闭连接。这个问题被称为 TCP 流的行头阻塞(HoL)。
        TCP间歇性地挂起数据传输
      • TCP 不支持流级复:虽然 TCP 确实允许在应用层之间建立多个逻辑连接,但它不允许在一个 TCP 流中复用数据包。使用 HTTP/2 时,浏览器只能与服务器打开一个 TCP 连接,并使用同一个连接来请求多个对象,如 CSSJavaScript 等文件。在接收这些对象的同时,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傻傻分不清楚,一文帮你理清前端渲染方案!
什么是强缓存和协商缓存
你会怎么做前端优化?

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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值