你必须懂的前端性能优化

从输入URL加载起看方向

从输入 URL 到页面加载完成的过程:javascript

  1. 首先作 DNS 查询,若是这一步作了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,而后下发给网络层。网络层中的 IP 协议会肯定 IP 地址,而且指示了数据传输中如何跳转路由器。而后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  3. TCP 握手结束后会进行 TLS 握手,而后就开始正式的传输数据
  4. 数据在进入服务端以前,可能还会先通过负责负载均衡的服务器,它的做用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  5. 首先浏览器会判断状态码是什么,若是是 200 那就继续解析,若是 400 或 500 的话就会报错,若是 300 的话会进行重定向,这里会有个重定向计数器,避免过屡次的重定向,超过次数也会报错
  6. 浏览器开始解析文件,若是是 gzip 格式的话会先解压一下,而后经过文件的编码格式知道该如何去解码文件
  7. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。若是遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,而后等待 HTML 解析完成后顺序执行,若是以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里若是使用 HTTP 2.0 协议的话会极大的提升多图的下载效率。
  8. 初始的 HTML 被彻底加载和解析后会触发 DOMContentLoaded 事件
  9. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是肯定页面元素的布局、样式等等诸多方面的东西
  10. 在生成 Render 树的过程当中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了

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

  • DNS 解析
  • TCP 链接
  • HTTP 请求/响应

对于 DNS 解析和 TCP 链接两个步骤,咱们前端能够作的努力很是有限。相比之下,HTTP 链接这一层面的优化才是咱们网络优化的核心。前端

HTTP 优化有两个大的方向:java

  • 减小请求次数
  • 减小单次请求所花费的时间

浏览器缓存策略

浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列以下:算法

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

MemoryCache

MemoryCache,是指存在内存中的缓存。从优先级上来讲,它是浏览器最早尝试去命中的一种缓存。从效率上来讲,它是响应速度最快的一种缓存。浏览器秉承的是“节约原则”,咱们发现,Base64 格式的图片,几乎永远能够被塞进 memory cache,这能够视做浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的概率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们每每被直接甩进磁盘。后端

Service Worker Cache

Service Worker 是一种独立于主线程以外的 Javascript 线程。它脱离于浏览器窗体,所以没法直接访问 DOM。这样独立的个性使得 Service Worker 的“我的行为”没法干扰页面的性能,这个“幕后工做者”能够帮咱们实现离线缓存、消息推送和网络代理等功能。咱们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。浏览器

HTTP Cache

它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存。缓存

对一条http get 报文的基本缓存处理过程包括7个步骤:服务器

  1. 接收
  2. 解析
  3. 查询,缓存查看是否有本地副本可用,若是没有,就获取一份副本
  4. 新鲜度检测, 缓存查看已缓存副本是否足够新鲜,若是不是,就询问服务器是否有任何更新。
  5. 建立响应,缓存会用新的首部和已缓存的主体来构建一条响应报文。
  6. 发送,缓存经过网络将响应发回给客服端。
  7. 日志

图片描述网络

强缓存

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通讯。

是否足够新鲜时期:
经过 Expires: XXXX XXX XXX GMT (绝对日期时间,http/1.0) 或者 Cache-Control:max-age=XXXX (相对日期时间,http/1.1)在文档标明过时日期。

Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,咱们以 Cache-Control 为准。

关键字理解

public 与 private 是针对资源是否可以被代理服务缓存而存在的一组对立概念。若是咱们为资源设置了 public,那么它既能够被浏览器缓存,也能够被代理服务器缓存;若是咱们设置了 private,则该资源只能被浏览器缓存。private 为默认值。

no-store与no-cache,no-cache 绕开了浏览器:咱们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存状况,而是直接向服务端去确认该资源是否过时(即走咱们下文即将讲解的协商缓存的路线)。no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只容许你直接向服务端发送请求、并下载完整的响应。

协商缓存

协商缓存依赖于服务端与浏览器之间的通讯。协商缓存机制下,浏览器须要向服务器去询问缓存的相关信息,进而判断是从新发起请求、下载完整的响应,仍是从本地获取缓存的资源。若是服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种状况下网络请求对应的状态码是 304。

协商缓存的实现:从 Last-Modified 到 Etag,详细本身百度,这里再也不详细展开。

HTTP 缓存决策

图片描述

当咱们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;不然考虑是否每次都须要向服务器进行缓存有效确认,若是须要,那么设 Cache-Control 的值为 no-cache;不然考虑该资源是否能够被代理服务器缓存,根据其结果决定是设置为 private 仍是 public;而后考虑该资源的过时时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存须要用到的 Etag、Last-Modified 等参数。

Push Cache

Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。

  • Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的状况下才会去询问 Push Cache。
  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不一样的页面只要共享了同一个 HTTP2 链接,那么它们就能够共享同一个 Push Cache。

CDN了解一番

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

“缓存”就是说咱们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现本身没有这个资源(通常是缓存的数据过时了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN 每每被用来存放静态资源。所谓“静态资源”,就是像 JS、CSS、图片等不须要业务服务器进行计算即得的资源。而“动态资源”,顾名思义是须要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染获得的 HTML 页面。

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

另外,CDN的域名必须和主业务服务器的域名不同,要不,同一个域名下面的Cookie各处跑,浪费了性能流量的开销,CDN域名放在不一样的域名下,能够完美地避免了没必要要的 Cookie 的出现!

图片优化

二进制位数与色彩的关系

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

一个二进制位表示两种颜色(0|1 对应黑|白),若是一种图片格式对应的二进制位数有 n 个,那么它就能够呈现 2^n 种颜色。

计算图片大小

对于一张 100 100 像素的图片来讲,图像上有 10000 个像素点,若是每一个像素的值是 RGBA 存储的话,那么也就是说每一个像素有 4 个通道,每一个通道 1 个字节(8 位 = 1个字节),因此该图片大小大概为 39KB(10000 1 * 4 / 1024)。
可是在实际项目中,一张图片可能并不须要使用那么多颜色去显示,咱们能够经过减小每一个像素的调色板来相应缩小图片的大小。
了解了如何计算图片大小的知识,那么对于如何优化图片,想必你们已经有 2 个思路了:

  • 减小像素点
  • 减小每一个像素点可以显示的颜色

图片类型要点

JPEG/JPG 特色:有损压缩、体积小、加载快、不支持透明,JPG 最大的特色是有损压缩。这种高效的压缩算法使它成为了一种很是轻巧的图片格式。另外一方面,即便被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当咱们把图片体积压缩至原有体积的 50% 如下时,JPG 仍然能够保持住 60% 的品质。但当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩致使的图片模糊会至关明显。

PNG 特色:无损压缩、质量高、体积大、支持透明,PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照咱们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的能够呈现约 1600 万种颜色。PNG 图片具备比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文咱们提到的 JPG 的局限性,惟一的 BUG 就是体积太大。

SVG 特色:文本文件、体积小、不失真、兼容性好,SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文说起的其它图片种类有着本质的不一样:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。

Base64 特色:文本文件、依赖编码、小图标解决方案,Base64 并不是一种图片格式,而是一种编码方式。Base64 和雪碧图同样,是做为小图标解决方案而存在的。

WebP 特色:年轻的全能型选手,WebP 像 JPEG 同样对细节丰富的图片信手拈来,像 PNG 同样支持透明,像 GIF 同样能够显示动态图片——它集多种图片文件格式的优势于一身。可是毕竟年轻,兼容性存在一些问题。

渲染优化

客户端渲染

在客户端渲染模式下,服务端会把渲染须要的静态文件发送给客户端,客户端加载过来以后,本身在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM。页面上呈现的内容,你在 html 源文件里里找不到——这正是它的特色。

服务端渲染

在服务端渲染的模式下,当用户第一次请求页面时,由服务器把须要的组件或页面渲染成HTML字符串,而后把它返回给客户端。页面上呈现的内容,咱们在 html 源文件里也能找到。服务端渲染解决了一个很是关键的性能问题——首屏加载速度过慢,也解决了SEO搜索引擎的问题。

浏览器渲染过程解析

浏览器的渲染机制通常分为如下几个步骤:

  1. 处理 HTML 并构建 DOM 树。
  2. 处理 CSS 构建 CSSOM 树
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,计算每一个节点的位置。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上。

在渲染DOM的时候,浏览器所作的工做其实是:

  1. 获取DOM后分割为多个图层
  2. 对每一个图层的节点计算样式结果(Recalculate style–样式重计算)
  3. 为每一个节点生成图形和位置(Layout–回流和重布局)
  4. 将每一个节点绘制填充到图层位图中(Paint Setup和Paint–重绘)
  5. 图层做为纹理上传至GPU
  6. 复合多个图层到页面上生成最终屏幕图像(Composite Layers–图层重组)

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

CSS 选择符是从右到左进行匹配的,好比 #myList li {}实际开销至关高。

  • 避免使用通配符,只对须要用到的元素进行选择。
  • 关注能够经过继承实现的属性,避免重复匹配重复定义。
  • 少用标签选择器。若是能够,用类选择器替代。 错误:#dataList li{} 正确:.dataList{}
  • 不要多此一举,id 和 class 选择器不该该被多余的标签选择器拖后腿。错误:.dataList#title 正确: #title
  • 减小嵌套。后代选择器的开销是最高的,所以咱们应该尽可能将选择器的深度降到最低(最高不要超过三层),尽量使用类来关联每个标签元素。

CSS 的阻塞

CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程当中,不会渲染任何已处理的内容。即使 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK。咱们将 CSS 放在 head 标签里 和尽快 启用 CDN 实现静态资源加载速度的优化。

JS 的阻塞

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

DOM渲染优化

先了解回流和重绘

  • 回流:当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
  • 重绘:当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫作重绘。

重绘不必定致使回流,回流必定会致使重绘。回流比重绘作的事情更多,带来的开销也更大。在开发中,要从代码层面出发,尽量把回流和重绘的次数最小化。

例子剖析

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>DOM操做测试</title>
</head>
<body>
  <div id="container"></div>
</body>
</html>
for(var count=0;count<10000;count++){ 
  document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'  //咱们每一次循环都调用 DOM 接口从新获取了一次 container 元素,额外开销
}

进化一:

// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>我是一个小测试</span>'
}

进化二:

//减小没必要要的DOM更改
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){ 
  // 先对内容进行操做
  content += '<span>我是一个小测试</span>'
} 
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content

事实上,考虑JS 的运行速度,比 DOM 快得多这个特性。咱们减小 DOM 操做的核心思路,就是让 JS 去给 DOM 分压。

在 DOM Fragment 中,DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当作一个轻量版的 Document 使用,用于存储已排好版的或还没有打理好格式的XML片断。由于 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引发 DOM 树的从新渲染的操做(reflow),且不会致使性能等问题。
进化三:

let container = document.getElementById('container')
// 建立一个DOM Fragment对象做为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
  // span此时能够经过DOM API去建立
  let oSpan = document.createElement("span")
  oSpan.innerHTML = '我是一个小测试'
  // 像操做真实DOM同样操做DOM Fragment对象
  content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)

进化四:
当涉及到过万调数据进行渲染,并且要求不卡住画面,如何解决?
如何在不卡住页面的状况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就能够经过 requestAnimationFrame 来每 16 ms 刷新一次。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      控件
    </ul>
    <script>
      setTimeout(() => {
        // 插入十万条数据
        const total = 100000
        // 一次插入 20 条,若是以为性能很差就减小
        const once = 20
        // 渲染数据总共须要几回
        const loopCount = total / once
        let countOfRender = 0
        let ul = document.querySelector('ul')
        function add() {
          // 优化性能,插入不会形成回流
          const fragment = document.createDocumentFragment()
          for (let i = 0; i < once; i++) {
            const li = document.createElement('li')
            li.innerText = Math.floor(Math.random() * total)
            fragment.appendChild(li)
          }
          ul.appendChild(fragment)
          countOfRender += 1
          loop()
        }
        function loop() {
          if (countOfRender < loopCount) {
            window.requestAnimationFrame(add)
          }
        }
        loop()
      }, 0)
    </script>
  </body>
</html>

window.requestAnimationFrame() 方法告诉浏览器您但愿执行动画并请求浏览器在下一次重绘以前调用指定的函数来更新动画。该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。
注意:若您想要在下次重绘时产生另外一个动画画面,您的回调例程必须调用 requestAnimationFrame()。

Event Loop

咱们先了解javascript运行机制,对渲染是大有帮助的,能够看我历史文章JavaScript运行机制,
Javascript运行机制深刻

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。
常见的 macro-task 好比: setTimeout、setInterval、 setImmediate、script(总体代码)、 I/O 操做、UI 渲染等。
常见的 micro-task 好比: process.nextTick、Promise、MutationObserver 等。

例子分析:

// task是一个用于修改DOM的回调
setTimeout(task, 0)

上面代码,如今 task 被推入的 macro 队列。但由于 script 脚本自己是一个 macro 任务,因此本次执行完 script 脚本以后,下一个步骤就要去处理 micro 队列了,再往下就去执行了一次 render,必须等待下一次的loop。

Promise.resolve().then(task)

上面代码,咱们结束了对 script 脚本的执行,是否是紧接着就去处理 micro-task 队列了?micro-task 处理完,DOM 修改好了,紧接着就能够走 render 流程了——不须要再消耗多余的一次渲染,不须要再等待一轮事件循环,直接为用户呈现最即时的更新结果。

当咱们须要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。

上面说了重绘与回流,Event loop,但不少人不知道的是,重绘和回流其实和 Event loop 有关。

  1. 当 Event loop 执行完 Microtasks 后,会判断 document 是否须要更新。由于浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 而后判断是否有 resize 或者 scroll ,有的话会去触发事件,因此 resize 和 scroll 事件也是至少 16ms 才会触发一次,而且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画而且发送事件
  5. 判断是否有全屏操做事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差
  8. 更新界面
  9. 以上就是一帧中可能会作的事情。若是在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

节流与防抖

当用户进行滚动,触发scroll事件,用户的每一次滚动都将触发咱们的监听函数。函数执行是吃性能的,频繁地响应某个事件将形成大量没必要要的页面计算。所以,咱们须要针对那些有可能被频繁触发的事件做进一步地优化。节流与防抖就颇有必要了!

详细看历史文章防抖动与节流

相关文章
相关标签/搜索