阅读 3303

前端性能优化-页面加载渲染优化

参考资料:前端总结--性能优化
参考资料:js和css的加载造成阻塞化
参考资料:浏览器的工作原理
参考资料:css 阻塞 js阻塞
参考资料:分布渲染
参考资料:css加载会造成阻塞吗
参考资料:干货 | 十分钟读懂浏览器渲染流程

1 知识体系

1.1 从URL输入到页面加载


首先我们需要通过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户

将这个过程切分为如下的过程片段

  1. DNS 解析
  2. TCP 连接
  3. HTTP 请求抛出
  4. 服务端处理请求,HTTP 响应返回
  5. 浏览器拿到响应数据,解析响应内容,把解析的结果展示给用户

1.2 性能优化思维导图


2 网路篇(http)

输入 URL 到显示页面这个过程中,涉及到网络层面的,有三个主要过程:

  1. DNS 解析
  2. TCP 连接
  3. HTTP 请求/响应

2.1 DNS解析


一般来说,在前端优化中与 DNS 有关的有两点:

  1. 是减少DNS的请求次数,
  2. 是进行DNS预获取——DNS Prefetch

DNS 作为互联网的基础协议,其解析的速度似乎很容易被网站优化人员忽视。现在大多数新浏览器已经针对DNS解析进行了优化,典型的一次DNS解析需要耗费20-120 毫秒,减少DNS解析时间和次数是个很好的优化方式。

DNS Prefetching 是让具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能减少用户的等待时间,提升用户体验 。

默认情况下浏览器会对页面中和当前域名(正在浏览网页的域名)不在同一个域的域名进行预获取,并且缓存结果,这就是隐式的 DNS Prefetch。如果想对页面中没有出现的域进行预获取,那么就要使用显示 DNS Prefetch 了。

DNS预解析具体用法

//用meta信息来告知浏览器, 当前页面要做DNS预解析
<meta http-equiv="x-dns-prefetch-control" content="on">
//在页面header中使用link标签来强制对DNS预解析: 
<link rel="dns-prefetch" href="//www.zhix.net"> 
复制代码

注意:dns-prefetch需慎用,多页面重复DNS预解析会增加重复DNS查询次数,因为有开发者指出 禁用DNS 预读取能节省每月100亿的DNS查询 。

//如果需要禁止隐式的 DNS Prefetch
<meta http-equiv="x-dns-prefetch-control" content="off">
复制代码

2.2 HTTP


对于 DNS 解析和 TCP 连接两个步骤,我们前端可以做的努力非常有限。相比之下,HTTP 连接这一层面的优化才是我们网络优化的核心

HTTP 优化有两个大的方向

  1. 减少请求次数
  2. 减少单次请求所花费的时间

2.2.1减少HTTP请求次数

  • 图片:雪碧图,图标字体文件
  1. 雪碧图
    多张小图片合并为一张图,利用CSS -background-position调整图片显示位置

  2. 图标字体文件
    阿里图标

  • 合并JS和CSS文件
  1. webpack
  2. gulp
  3. grunt
  • 浏览器缓存
    如果图片或者脚本,样式文件内容比较固定,不经常被修改,那么,尽可能利用缓存技术,减少HTTP请求次数或文件下载次数
  1. 强缓存
  2. 协商缓存

2.2.2减少单次请求所花费的时间

3.网络篇(图片优化)

3.1不同业务场景下的图片方案选型


前置知识:二进制位数与色彩的关系 在计算机中,像素用二进制数来表示。不同的图片格式中像素与二进制位数之间的对应关系是不同的。一个像素对应的二进制位数越多,它可以表示的颜色种类就越多,成像效果也就越细腻,文件体积相应也会越大。

3.2 JPEG/JPG


关键字:有损压缩、体积小、加载快、不支持透明

  • JPG 的优点

    JPG 最大的特点是有损压缩。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉——前提是你用对了业务场景。

  • 使用场景

    JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。

    两大电商网站对大图的处理,是 JPG 图片应用场景的最佳写照: 打开淘宝首页,我们可以发现页面中最醒目、最庞大的图片,一定是以 .jpg 为后缀的: 使用 JPG 呈现大图,既可以保住图片的质量,又不会带来令人头疼的图片体积,是当下比较推崇的一种方案。

  • JPG 的缺陷

    有损压缩在上文所展示的轮播图上确实很难露出马脚,但当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。此外,JPEG 图像不支持透明度处理,透明图片需要召唤 PNG 来呈现。

3.3 png


关键字:无损压缩、质量高、体积大、支持透明

  • PNG 的优点

    PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。

    PNG 图片具有比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPG 的局限性,唯一的 BUG 就是体积太大。

  • PNG-8 与 PNG-24 的选择题

    什么时候用 PNG-8,什么时候用 PNG-24,这是一个问题

    理论上来说,当你追求最佳的显示效果、并且不在意文件体积大小时,是推荐使用 PNG-24 的。

    但实践当中,为了规避体积的问题,我们一般不用PNG去处理较复杂的图像。当我们遇到适合 PNG 的场景时,也会优先选择更为小巧的 PNG-8。

    如何确定一张图片是该用 PNG-8 还是 PNG-24去呈现呢?好的做法是把图片先按照这两种格式分别输出,看 PNG-8输出的结果是否会带来肉眼可见的质量损耗,并且确认这种损耗是否在我们(尤其是你的 UI 设计师)可接受的范围内,基于对比的结果去做判断。

  • 应用场景

    前面我们提到,复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,我们一般会交给 JPG 去存储。

    考虑到 PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。

    此时我们再次把目光转向性能方面堪称业界楷模的淘宝首页,我们会发现它页面上的 Logo,无论大小,还真的都是 PNG 格式:

3.4 SVG


关键字:文本文件、体积小、不失真、兼容性好

  • SVG 的使用方式与应用场景

    将 SVG 写入 HTML

    将 SVG 写入独立文件后引入 HTML将 SVG 写入 HTML

3.5 Base64


关键字:文本文件、依赖编码、小图标解决方案

  • Base64 的应用场景

    图片的实际尺寸很小(大家可以观察一下掘金页面的 Base64 图,几乎没有超过 2kb 的)

    图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)

    图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)

3.6 WebP


关键字:年轻的全能型选手 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩。

  • WebP 的优点

    WebP 像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身。

  • WebP 的局限性

    兼容性

3.7 总结


不同业务场景下的图片方案选型

4.存储篇(浏览器缓存)

4.1 什么是缓存


对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤

浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

缓存思维导图

4.2 缓存位置


4.2.1 缓存优先级

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

Service Worker
Memory Cache
Disk Cache
Push Cache

4.2.2 Service Worker

不了解 MDN

4.2.3 MemoryCache

MemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。

内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。

那么哪些文件会被放入内存呢?

事实上,这个划分规则,一直以来是没有定论的。不过想想也可以理解,内存是有限的,很多时候需要先考虑即时呈现的内存余量,再根据具体的情况决定分配给内存和磁盘的资源量的比重——资源存放的位置具有一定的随机性

虽然划分规则没有定论,但根据日常开发中观察的结果,包括我们开篇给大家展示的 Network 截图,我们至少可以总结出这样的规律:资源存不存内存,浏览器秉承的是“节约原则”。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。

4.2.4 Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中 对于大文件来说,大概率是不存储在内存中的,反之优先 当前系统内存使用率高的话,文件优先存储进硬盘

4.2.5 Push Cache

不了解

push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用

4.3 缓存过程分析


浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求,那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图:

上图我们可以知道

  1. 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  2. 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

4.4 http缓存

HTTP 缓存是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

4.5 强缓存

强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。

4.5.1 Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。

  1. 缓存过期时间,用来指定资源的到期时间,是服务器端的具体的时间点
  2. 告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而不用再次请求
  3. max-age的优化级高于expires,当有max-age的时候,会无视expires
  4. 当在有效时间内,如果服务器端的文件已经发生改变,但是浏览器端无法感知

4.5.2 Cache-Control

在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令

  1. max-age
  2. s-maxage
  3. private
  4. public
  5. no-cache
  6. no-store

  • max-age

    max-age=xxx (xxx is numeric)表示缓存内容将在xxx秒后失效

    1. 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间,
    2. 优先级高于Expires

  • s-maxage

    1. 覆盖max-age 或者 Expires头,但是仅适用于共享缓存(比如各个代理),并且私有缓存2中它被忽略
    2. 能用于public,如CDN
    3. 优先级高于max-age

  • private

    所有内容只有客户端可以缓存

    表示中间节点不允许缓存,对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会老老实实把Server 返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是自作主张给自己缓存的数据

    1. 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容
    2. 自己的服务器
  • public

    所有内容都将被缓存(客户端和代理服务器都可缓存)

    具体来说响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的proxy可以缓存资源,比如下次再请求同一资源proxy1直接把自己缓存的东西给 Browser 而不再向proxy2要。

  • no-store

    所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

    1. 缓存不应存储有关客户端请求或服务器响应的任何内容。
    2. 不会使用任何缓存策略
  • no-cache

    客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致

    1. 释放缓存副本之前,强制高速缓存将请求提交给原始服务器进行验证
    2. 这个文件不管怎么样,都会向服务器发起请求,去服务器哪边询问,这个文件有没有在缓存策略里

4.5.3 强缓存思维导图

强缓存思维导图

4.6 协商缓存


4.6.1 什么是协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

协商缓存生效,返回304和Not Modified

协商缓存失效,返回200和请求结果

4.6.2 Last-Modified和If-Modified-Since

Last-Modified 是一个响应首部,其中包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。

  1. 基于客户端和服务端协商的缓存机制
  2. Last-Modified ----response header
  3. If-Modified-Since----request header
  4. 需要与cache-control共同使用
  5. max-age的优先级高于Last-Modified
  • 缺点
  1. 某些服务端不能获取精确的修改时间
  2. 文件修改时间改了,但文件内容却没有变

4.6.3 Etag/If-None-Match

ETagHTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖(“空中碰撞”)

  1. 文件内容的hash值
  2. etag--response header
  3. if-none-match -- request header
  4. 要与cache-control共同使用

4.6.4两者对比

  1. 首先在精确度上,Etag要优于Last-Modified。
  2. 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  3. 第三在优先级上,服务器校验优先考虑Etag

4.6.5 思维导图

4.7 参考


浏览器缓存机制介绍与缓存策略剖析
一文读懂前端缓存
深入理解浏览器的缓存机制

5. 存储篇(本地存储)

5.1 Cookie


  1. Cookie 的本职工作并非本地存储,而是“维持状态”。
  2. Cookie 不够大
  3. 同一个域名下的所有请求,都会携带 Cookie

5.2 Local Storage,Session Storage


5.2.1 概述

存储容量大: Web Storage 根据浏览器的不同,存储容量可以达到 5-10M 之间 仅位于浏览器端,不与服务端发生通信。

5.2.2 Local Storage 与 Session Storage 的区别

  • 生命周期: Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

  • 作用域: Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。

5.2.3 应用场景

  • Local Storage

    1. 理论上 Cookie 无法胜任的、可以用简单的键值对来存取的数据存储任务,都可以交给 Local Storage 来做。
    2. 存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串
    3. 存储一些不经常更新的 CSS、JS 等静态资源
  • Session Storage

    微博的 Session Storage 就主要是存储你本次会话的浏览足迹

5.3 IndexedDB


  1. IndexedDB 是没有存储上限的(一般来说不会小于 250M)
  2. IndexedDB 可以看做是 LocalStorage 的一个升级,当数据的复杂度和规模上升到了 LocalStorage 无法解决的程度,我们毫无疑问可以请出 IndexedDB 来帮忙。

6. CDN

6.1 什么是CDN


CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。

6.2 为什么要用 CDN


缓存、本地存储带来的性能提升,是不是只能在“获取到资源并把它们存起来”这件事情发生之后?也就是说,首次请求资源的时候,这些招数都是救不了我们的。要提升首次请求的响应能力,我们还需要借助 CDN 的能力

6.3 CDN 如何工作


假设我的根服务器在杭州,同时在图示的五个城市里都有自己可用的机房

此时有一位北京的用户向我请求资源。在网络带宽小、用户访问量大的情况下,杭州的这一台服务器或许不那么给力,不能给用户非常快的响应速度。于是我灵机一动,把这批资源 copy 了一批放在北京的机房里。当用户请求资源时,就近请求北京的服务器,北京这台服务器低头一看,这个资源我存了,离得这么近,响应速度肯定噌噌的!那如果北京这台服务器没有 copy 这批资源呢?它会再向杭州的根服务器去要这个资源。在这个过程中,北京这台服务器就扮演着 CDN 的角色。

6.4 CDN的核心功能


CDN 的核心点有两个,一个是缓存,一个是回源。

  • 缓存

    “缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程

  • 回源

    就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。

6.5 CDN 与前端性能优化


CDN 往往被用来存放静态资源

“根服务器”本质上是业务服务器,它的核心任务在于生成动态页面或返回非纯静态页面,这两种过程都是需要计算的。业务服务器仿佛一个车间,车间里运转的机器轰鸣着为我们产出所需的资源;相比之下,CDN 服务器则像一个仓库,它只充当资源的“栖息地”和“搬运工”。

所谓“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。而“动态资源”,顾名思义是需要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染得到的 HTML 页面。

什么是“非纯静态资源”呢?它是指需要服务器在页面之外作额外计算的 HTML 页面。具体来说,当我打开某一网站之前,该网站需要通过权限认证等一系列手段确认我的身份、进而决定是否要把 HTML 页面呈现给我。这种情况下 HTML 确实是静态的,但它和业务服务器的操作耦合,我们把它丢到CDN 上显然是不合适的。

6.6 CDN 的实际应用


静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段,在许多一线的互联网公司,“静态资源走 CDN”并不是一个建议,而是一个规定

www.taobao.com/

可以看到业务服务器确实是返回给了我们一个尚未被静态资源加持过的简单 HTML 页面

随便点开一个静态资源,可以看到它都是从 CDN 服务器上请求来的

6.7 cdn与cookie


Cookie 是紧跟域名的。同一个域名下的所有请求,都会携带 Cookie。大家试想,如果我们此刻仅仅是请求一张图片或者一个 CSS 文件,我们也要携带一个 Cookie 跑来跑去(关键是 Cookie 里存储的信息我现在并不需要),这是一件多么劳民伤财的事情。Cookie 虽然小,请求却可以有很多,随着请求的叠加,这样的不必要的 Cookie 带来的开销将是无法想象的

同一个域名下的请求会不分青红皂白地携带 Cookie,而静态资源往往并不需要 Cookie 携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!

看起来是一个不起眼的小细节,但带来的效用却是惊人的。以电商网站静态资源的流量之庞大,如果没把这个多余的 Cookie 拿下来,不仅用户体验会大打折扣,每年因性能浪费带来的经济开销也将是一个非常恐怖的数字。

7.渲染篇(服务端渲染)

7.1 客户端渲染


客户端渲染模式下,服务端会把渲染需要的静态文件发送给客户端,客户端加载过来之后,自己在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM

<!doctype html>
<html>
  <head>
    <title>我是客户端渲染的页面</title>
  </head>
  <body>
    <div id='root'></div>
    <script src='index.js'></script>
  </body>
</html>
复制代码

根节点下到底是什么内容呢?你不知道,我不知道,只有浏览器把 index.js 跑过一遍后才知道,这就是典型的客户端渲染。

页面上呈现的内容,你在 html 源文件里里找不到——这正是它的特点。

7.2 服务端渲染


服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。

使用服务端渲染的网站,可以说是“所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到

7.3 服务端渲染解决了什么性能问题


事实上,很多网站是出于效益(seo)的考虑才启用服务端渲染,性能倒是在其次。

假设 A 网站页面中有一个关键字叫“前端性能优化”,这个关键字是 JS 代码跑过一遍后添加到 HTML 页面中的。那么客户端渲染模式下,我们在搜索引擎搜索这个关键字,是找不到 A 网站的——搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。A 网站的运营方见此情形,感到很头大:搜索引擎搜不出来,用户找不到我们,谁还会用我的网站呢?为了把“现成的内容”拿给搜索引擎看,A 网站不得不启用服务端渲染。

但性能在其次,不代表性能不重要。服务端渲染解决了一个非常关键的性能问题——首屏加载速度过慢。在客户端渲染模式下,我们除了加载 HTML,还要等渲染所需的这部分 JS 加载完,之后还得把这部分 JS 在浏览器上再跑一遍。这一切都是发生在用户点击了我们的链接之后的事情,在这个过程结束之前,用户始终见不到我们网页的庐山真面目,也就是说用户一直在等!相比之下,服务端渲染模式下,服务器给到客户端的已经是一个直接可以拿来呈现给用户的网页,中间环节早在服务端就帮我们做掉了,用户岂不“美滋滋”?。

7.4 服务端渲染的应用场景


服务端渲染本质上是本该浏览器做的事情,分担给服务器去做。这样当资源抵达浏览器时,它呈现的速度就快了

但仔细想想,在这个网民遍地的时代,几乎有多少个用户就有多少台浏览器。用户拥有的浏览器总量多到数不清,那么一个公司的服务器又有多少台呢?我们把这么多台浏览器的渲染压力集中起来,分散给相比之下数量并不多的服务器,服务器肯定是承受不住的

除非网页对性能要求太高了,以至于所有的招式都用完了,性能表现还是不尽人意,这时候我们就可以考虑向老板多申请几台服务器,把服务端渲染搞起来了~

8.渲染篇(浏览器渲染)

8.1 浏览器内核


浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎

渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。

8.2 浏览器渲染过程解析


8.3 基于渲染流程的 CSS 优化建议


8.3.1 CSS 选择符是从右到左进行匹配的

#myList li {}
复制代码

浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList

8.3.2 具体优化

  1. 避免使用通配符,只对需要用到的元素进行选择
  2. 关注可以通过继承实现的属性,避免重复匹配重复定义。
  3. 少用标签选择器。如果可以,用类选择器替代
  4. 不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿
  5. 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素

8.4 告别阻塞:CSS 与 JS 的加载顺序优化


HTML、CSS 和 JS,都具有阻塞渲染的特性。 HTML 阻塞,天经地义——没有 HTML,何来 DOM?没有 DOM,渲染和优化,都是空谈。

8.4.1 CSS 的阻塞

在刚刚的过程中,我们提到 DOM 和 CSSOM 合力才能构建渲染树。这一点会给性能造成严重影响:默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了避免没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)。

只有当我们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。 很多时候,DOM 不得不等待 CSSOM。因此我们可以这样总结:

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)

  1. css文件是并行下载的
  2. css的下载会阻塞后面js的执行
  3. css的下载不会阻塞后面的html的解析,但是会阻塞dom渲染
  4. css的下载会阻塞后面的DOM的onload事件。
  5. css的下载不会阻塞后面js的下载,但是js下载完成后,被阻塞执行。

8.4.2 JS 的阻塞

JS 的作用在于修改,它帮助我们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。因此 JS 的执行会阻止 CSSOM,在我们不作显式声明的情况下,它也会阻塞 DOM。

JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。

  1. 现代浏览器会并行加载js文件。

  2. 加载或者执行js时会阻塞对标签的解析,也就是阻塞了dom树的形成,只有等到js执行完毕,浏览器才会继续解析标签。没有dom树,浏览器就无法渲染,所以当加载很大的js文件时,可以看到页面很长时间是一片空白

之所以会阻塞对标签的解析是因为加载的js中可能会创建,删除节点等,这些操作会对dom树产生影响,如果不阻塞,等浏览器解析完标签生成dom树后,js修改了某些节点,那么浏览器又得重新解析,然后生成dom树,性能比较差

8.4.3 JS的三种加载方式

正常模式

这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。

<script src="index.js"></script>
复制代码

async(异步) 模式

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。

<script async src="index.js"></script>
复制代码

defer(延缓) 模式

efer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

<script defer src="index.js"></script>
复制代码

从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer。

9. 渲染篇(dom优化)

10. 渲染篇(Event Loop与异步更新策略(vue))

10.1 什么是异步更新?


当我们使用 Vue 或 React 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。

异步更新可以帮助我们避免过度渲染,是我们上节提到的“让 JS 为 DOM 分压”的典范之一。

10.2 异步更新的优越性


异步更新的特性在于它只看结果,因此渲染引擎不需要为过程买单

有时我们会遇到这样的情况

// 任务一
this.content = '第一次测试'
// 任务二
this.content = '第二次测试'
// 任务三
this.content = '第三次测试'
复制代码

我们在三个更新任务中对同一个状态修改了三次,如果我们采取传统的同步更新策略,那么就要操作三次 DOM。但本质上需要呈现给用户的目标内容其实只是第三次的结果,也就是说只有第三次的操作是有意义的——我们白白浪费了两次计算。

但如果我们把这三个任务塞进异步更新队列里,它们会先在 JS 的层面上被批量执行完毕。当流程走到渲染这一步时,它仅仅需要针对有意义的计算结果操作一次 DOM——这就是异步更新的妙处。

10.3 Vue状态更新手法:nextTick


Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际工作

$nextTick就是用来知道什么时候DOM更新完成的

11. 渲染篇(回流与重绘)

11.1 reflow与repaint


根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。这时就要提到两个重要概念:replaint和reflow。

replaint:屏幕的一部分重画,不影响整体布局,比如某个CSS的背景色变了,但元素的几何尺寸和位置不变。

reflow: 意味着元件的几何尺寸变了,我们需要重新验证并计算渲染树。是渲染树的一部分或全部发生了变化。这就是Reflow,或是Layout。

所以我们应该尽量减少reflow和replaint,我想这也是为什么现在很少有用table布局的原因之一。

display:none 会触发 reflow,visibility: hidden属性并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,它会被渲染成一个空框,所以visibility:hidden 只会触发 repaint,因为没有发生位置变化。

有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。

有些情况下,比如 resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。

12.2 节流与防抖的本质


这两个东西都以闭包的形式存在。

它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

12.3 节流


在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

现在有一个旅客刚下了飞机,需要用车,于是打电话叫了该机场唯一的一辆机场大巴来接。司机开到机场,心想来都来了,多接几个人一起走吧,这样这趟才跑得值——我等个十分钟看看。于是司机一边打开了计时器,一边招呼后面的客人陆陆续续上车。在这十分钟内,后面下飞机的乘客都只能乘这一辆大巴,十分钟过去后,不管后面还有多少没挤上车的乘客,这班车都必须发走

12.4 防抖


在某段时间内,不管你触发了多少次回调,我都只认最后一次。

第一个乘客上车后,司机开始计时(比如说十分钟)。十分钟之内,如果又上来了一个乘客,司机会把计时器清零,重新开始等另一个十分钟(延迟了等待)。直到有这么一位乘客,从他上车开始,后续十分钟都没有新乘客上车,司机会认为确实没有人需要搭这趟车了,才会把车开走。

12.5 防抖的缺憾


防抖 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 防抖 设置的 delay 时间结束就进行下一次操作,于是每次 防抖 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。 为了避免弄巧成拙,我们需要借力 节流 的思想,打造一个“有底线”的 防抖——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 节流与 防抖 “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 节流 函数的实现中

11 性能监控

performance

function getsec(time) {
  return time / 1000 + 's'
}
window.onload = function () {
  var per = window.performance.timing;
  console.log('DNS查询耗时' + getsec(per.domainLookupEnd - per.domainLookupStart))
  console.log('TCP链接耗时' + getsec(per.connectEnd - per.connectStart))
  console.log('request请求响应耗时' + getsec(per.responseEnd - per.responseStart))
  console.log('dom渲染耗时' + getsec(per.domComplete - per.domInteractive))
  console.log('白屏时间' + getsec(firstPaint - pageStartTime))
  console.log('domready可操作时间' + getsec(per.domContentLoadedEventEnd - per.fetchStart))
}
复制代码
文章分类
前端
文章标签