Skip to content

前端性能优化课件

Published: at 02:13 AM

Table of contents

Open Table of contents

前言

聊一聊 前端性能优化

性能优化想必大家都知道,谈及性能优化,最直接的两个问题自然就是什么地方需要优化,以及怎么去优化!

在谈及这两个核心的问题之前,我们先来聊一聊,为什么需要性能优化?

性能优化是每一个开发者所必须面对的问题,随着为我们学习的深入,代码量越来越多,逻辑越来越复杂,对于资源的占用也越来越高。在这个时候,可能你自己的设备可以正常的运行。但是,一个产品是要面向用户的。如果在用户的设备上无法流畅运行的话,那么这个产品就不会是一个成功的产品。在这种时候,性能优化在这个时候就非常有必要性。

什么地方需要性能优化?

聊及这个问题,那就不得不提及那个经典的问题

从URL输入到页面加载完成,都发生了什么?

这个问题非常重要,首先,只有我们知道URL加载过程发生了什么,我们就能对这个过程进行分析,解读,从而进行优化!

站在性能优化的角度,我们来解读一下这个问题。首先,客户端是通过IP和服务端建立连接的。所以说,第一步就是要通过DNS(域名解析系统)将URL转换为对应的IP地址。然后,我们通过这个IP地址,与其所对应的服务器建立起了TCP连接。建立起了连接之后,客户端就会向服务端发起一个http请求,服务端接收到我们的数据之后,就会把我们要请求的数据放在http请求的响应里。到了这个时候,浏览器就可以根据拿到的响应,将内容解析,渲染到浏览器上,展示给用户。

总结一下,上面这一大段话总体来说分为五个阶段

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

So,如果要对浏览器进行优化,这为五个地方是绕不开的。根据这几个方向进行处理,就可以很好的进行性能优化。

对应优化

DNS解析

俗话说得好,有DNS的地方,就有缓存。

简单的回顾一下DNS

全称 Domain Name System ,即域名系统。万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。

通过域名,最终得到该域名对应的IP地址的过程叫做域名解析(或主机名解析)。

www.baidu.com (域名) - DNS解析 -> 39.156.69.79 (IP地址)

DNS缓存

浏览器、操作系统、Local DNS、根域名服务器,它们都会对DNS结果做一定程度的缓存。

DNS查询过程如下:

  1. 首先搜索浏览器自身的DNS缓存,如果存在,则域名解析到此完成。
  2. 如果浏览器自身的缓存里面没有找到对应的条目,那么会尝试读取操作系统的hosts文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。
  3. 如果本地hosts文件不存在映射关系,则查找本地DNS服务器(ISP服务器,或者自己手动设置的DNS服务器),如果存在,域名到此解析完成。
  4. 如果本地DNS服务器还没找到的话,它就会向根服务器发出请求,进行递归查询。

浏览器在获取网站域名的实际IP地址后会对其IP进行缓存,减少网络请求的损耗。每种浏览器都有一个固定的DNS缓存时间,其中Chrome的过期时间是1分钟,在这个期限内不会重新请求DNS。Chrome浏览器看本身的DNS缓存时间比较方便,在地址栏输入:

chrome://net-internals/#dns

DNS prefetch

提前解析之后可能会用到的域名,使解析结果缓存到系统缓存(Chromium)中,缩短DNS解析时间,来提高网站的访问速度。

DNS Prefetch 主要分为两个步骤

  1. html 源码下载完成后,会解析页面的包含链接的标签,提前查询对应的域名。
  2. 对于访问过的页面,浏览器会记录一份域名列表,当再次打开时,会在 html 下载的同时去解析 DNS。

DNS预解析分为自动解析和主动解析

TCP链接

长连接、预连接

预连接

preconnect 允许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析,TLS 协商,TCP 握手,这消除了往返延迟并为用户节省了时间。可以这样使用:

<link href="https://cdn.domain.com" rel="preconnect" crossorigin />

preconnect.png

一些其他的pre

PreRender

这是一个非常猛的预加载,因为它可以加载文档的所有资源

<link rel="prerender" href="http://example.com" />
Preload

preload 顾名思义就是一种预加载的方式,它通过向浏览器声明一个需要提交加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗。

这和浏览器的预加载不同,浏览器的预加载HTML里声明的资源。而perload实际上已经突破了这个限制,它还可以加载在CSS和JavaScript中的资源。

用法:

<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style" />

<!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
  const link = document.createElement("link");
  link.rel = "preload";
  link.as = "style";
  link.href = "/path/to/style.css";
  document.head.appendChild(link);
</script>

当浏览器解析到这些代码就会去加载 href 中对应的资源但不执行,待到真正使用到的时候再执行。

Prefetch

prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源。它的用法跟 preload 是一样的:

<link rel="prefetch" href="/path/to/style.css" as="style" />

注意:

  1. 不要将 preload 和 prefetch 进行混用,它们分别适用于不同的场景,对于同一个资源同时使用 preload 和 prefetch 会造成二次的下载。
  2. preload 字体不带 crossorigin 也将会二次获取! 确保你对 preload 的字体添加 crossorigin 属性,否则他会被下载两次,这个请求使用匿名的跨域模式。这个建议也适用于字体文件在相同域名下,也适用于其他域名的获取(比如说默认的异步获取)。

能看得见的优化——图片优化

最简单的图片优化——压缩图片

图片压缩工具:https://tinypng.com/

2021年,前端图片资源占比不断增加

推荐看看 HTTP-Archive 这个网站,它可以定期抓取Web站点,并记录资源使用的情况。我们可以看到世界范围内Web站点资源变化的趋势。

截止到2021年5月14日,过去一年Web资源请求的结果是这样的

我们可以看到,过去的一年,Web请求资源的大小提升的十分迅速。

而具体到图片这一个选项

虽然增长的没有总体资源那么迅速,但是我们可以看到,它的占比是十分巨大的,仅仅是单个的图片资源,就占了全部资源将近一半的比例。

所以,如果我们在图片优化优化方面有了长足的进步,那么对于前端方面整体的性能优化会有相当大的提升。

前端的图片选择

JPEG/JPG

广大的电商网站(例如淘宝),首页最醒目的,色彩最丰富的大图,往往使用的都是JPG

PNG-8 与 PNG-24

SVG

Base64

在讲Base64之前,我们先说一说另一种前端图片解决方案——雪碧图(CSS Sprites)

雪碧图、CSS 精灵、CSS Sprites、图像精灵,说的都是这个东西——一种将小图标和背景图像合并到一张图片上,然后利用 CSS 的背景定位来显示其中的每一部分的技术。

MDN上的解释:图像精灵(sprite,意为精灵),被运用于众多使用大量小图标的网页应用之上。它可取图像的一部分来使用,使得使用一个图像文件替代多个小文件成为可能。相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。

通过上面的介绍,我们知道了,雪碧图的核心其实就是通过把许多小图片放在一张大图上,减少请求图片的次数。Base64也是类似

Base64 并非一种图片格式,而是一种编码方式。Base64 和雪碧图一样,是作为小图标解决方案而存在的。

Base64的工具推荐——webpack 的 url-loader

WebP

WebP 是今天在座各类图片格式中最年轻的一位,它于 2010 年被提出, 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩。

浏览器相关

之前浏览器的一些基础知识里已经说过了。这里主要是一些复习和补充

复习一些和浏览器的知识

进程和线程

  1. 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)

  2. 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

浏览器是多进程的

浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)

简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。

在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了 (所以每一个Tab标签对应一个进程并不一定是绝对的)

浏览器的渲染过程

这张图对于浏览器的渲染过程有一个较为清晰的描述,其中的了解渲染树对浏览器渲染的理解会有一个更为清晰的认识!

浏览器缓存机制

缓存从位置上来说,分为四种,当我们查找缓存数据找不到的时候,我们才会去请求网络

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

Memory Cache

Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

但我们要知道,内存是非常宝贵的资源,其中可以提供给浏览器使用的缓存是非常小的,所以我们在使用这一部分的缓存的时候一定要精打细算。

我们之前提到的一个pre prefetch 就是存在这个缓存里的。

Dish Cache

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

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

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

Push Cache 的一些特性:

缓存的选择

大多数缓存文件都是存储在Disk Cache中的

用户行为对浏览器缓存的影响

本地储存

Cookie 的本职工作并非本地存储,而是“维持状态”。

在 Web 开发的早期,人们亟需解决的一个问题就是状态管理的问题:HTTP 协议是一个无状态协议,服务器接收客户端的请求,返回一个响应,故事到此就结束了,服务器并没有记录下关于客户端的任何信息。那么下次请求的时候,如何让服务器知道“我是我”呢?

在这样的背景下,Cookie 应运而生。

Cookie 说白了就是一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。

劣势

  1. 不够大

    大家知道,Cookie 是有体积上限的,它最大只能有 4 KB。当 Cookie 超过 4 KB 时,它将面临被裁切的命运。这样看来,Cookie 只能用来存取少量的信息。

  2. 过量的Cookie会造成大量的性能浪费

随着前端应用复杂度的提高,Cookie 也渐渐演化为了一个“存储多面手”——它不仅仅被用于维持状态,还被塞入了一些乱七八糟的其它信息,被迫承担起了本地存储的“重任”。在没有更好的本地存储解决方案的年代里,Cookie 小小的身体里承载了 4 KB 内存所不能承受的压力。

为了弥补 Cookie 的局限性,让“专业的人做专业的事情”,Web Storage 出现了。

Web Storage

Web Storage 是 HTML 5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。这两组概念非常相近,我们不妨先理解它们之间的区别,再对它们的共性进行研究。

Local Storage 与 Session Storage 的区别

两者的区别在于生命周期作用域的不同。

Web Storage 的特性

IndexedDB

IndexedDB 是一个运行在浏览器上的非关系型数据库。既然是数据库了,那就不是 5 M、10 M 这样小打小闹级别了。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250 M)。它不仅可以存储字符串,还可以存储二进制数据。

IndexedDB 可以看做是 Local Storage 的一个升级,当数据的复杂度和规模上升到了 Local Storage 无法解决的程度,我们毫无疑问可以请出 IndexedDB 来帮忙。

在一些需要写博客的网站,比如CSDN、慕课网 都看到了有使用 IndexedDB,用 IndexedDB 来存储文章草稿似乎很合适。

HTTP相关

针对网络优化,我们应该做的

HTTP管道

在HTTP 1.1中,新增了两个十分有用的特性

HTTP 1.1 管道的好处就是消除了发送请求和响应的等待时间。这种并行处理请求的能力对于应用性能的提升相当的大。

我们向服务器发送多个请求,服务器会先处理第一个请求,然后返回响应。接着,处理第二个请求,然后返回第二个响应。这样会产生相当多的往返延时。

持久的TCP连接获取数据:通过尽早的派发请求,不被每次响应阻塞,可以消除额外的网络往返。这样就从非持久连接下每次请求两次往返,变成了整个请求队列只需要两次往返。

进一步的,如果我们在服务器上并行处理请求的话,还能进一步的减少请求的时间。

但是,HTTP 1.x 的返回格式具有一定的局限性。我们虽然可以并行的发送请求。但是服务端并不允许相应信息交错到达。响应顺序是严格串行返回的,先发送的请求必定会先返回响应。即使后发送的请求先处理完成。

上图:

所以,虽然HTTP管道对性能的提升很明显,但因为队首阻塞的现象存在,产生了一系列的副作用

由于存在着这一系列的副作用,而HTTP 1.1 的标准中却并未对此做出说明。所以,尽管HTTP管道技术的优点毋庸置疑,但是应用仍然十分有限。一些支持管道功能的浏览器,通常都将其作为一个高级选项,但大多是浏览器通常会禁用它。

连接和拼合

最快的请求就是不用请求。不管使用什么协议,还是再什么类型的应用中,减少请求次数总是最好的性能优化手段。如果实在是无法减少网络请求的话,那么,对于HTTP 1.x 来言,可以考虑把多个资源打包捆绑到一块,然后通过一个网络请求来获取。

对于JavaScript和CSS来说,只要保持一定的顺序,就可以做到在保证代码的性能和执行的基础上把多个文件连接起来。对于图片来说,本文之前提到过的雪碧图就是这个思想的一个具体实现。

连接和拼合技术都是属于以内容为中心的应用层优化,它们通过减少网络的往返开销,可以获得明显的性能提升。然而,实现这些技术也需要额外的处理,部署和编码,因而会给应用带来额外的复杂性。此外,把多个资源打包到一起,也可能对缓存带来额外的负担,影响页面的执行速度。

对于副作用的理解,我们举一个简单的例子:一个包含了十来个JavaScript和CSS文件的应用,在产品状态下把所有的文件合并为一个JavaScript和一个CSS文件。

在实际中,大多数的Web应用不只有一个页面,而是由多个视图构成的。每个视图都有自己的资源,同时资源之间还有部分重叠(公用的CSS,JavaScript和图片)。其实,我们可以把它看成是一种预获取,代价是降低了初始的启动速度。

内存占用也是一个问题。对于雪碧图来说,浏览器必须分析整个图片,即使实际上只是显示了图片中的一小块,也要把整个图片保存在内存中。浏览器是不会把不显示的部分剔除掉的。

对于打包到多少大小合适,实际上并没有一个定论。然而,谷歌的测试团队PageSpeed表明,30~50 k(压缩后),是每个JavaScript文件比较合适的大小范围。即达到了能够减少小文件带来的网络延迟,还能确保文件的执行速度不会有太大的影响。当然,具体的结果可能会因为应用类型和脚本数量的影响有所不同。

我们所需要做的权衡

如何平衡两者之间的关系,从而达到一个最佳性能的平衡点,是一个艺术!

懒加载

初识

懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。

为什么要使用懒加载

实现原理

首先将页面上的图片的 src 属性设为空字符串,而图片的真实路径则设置在data-src属性中, 当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内将图片的src属性设置为data-src 的值,这样就可以实现延迟加载。

// 获取所有的图片标签
const imgs = document.getElementsByTagName("img");
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0;
function lazyload() {
  for (let i = num; i < imgs.length; i++) {
    // 用可视区域高度减去元素顶部距离可视区域顶部的高度
    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
    // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
    if (distance >= 0) {
      // 给元素写入真实的src,展示图片
      imgs[i].src = imgs[i].getAttribute("data-src");
      // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
      num = i + 1;
    }
  }
}
// 监听Scroll事件
window.addEventListener("scroll", lazyload, false);

CDN

初识

主要思路: 尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定

实现方法: 通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接和负载状况以及到用户的距离响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,加快访问速度

目的: 使用户可就近取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站的响应速度

优势:

  1. CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
  2. 大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。

工作原理

  1. 用户向浏览器输入www.taobao.com这个域名,浏览器第一次发现本地没有DNS缓存,则向网站的DNS服务器请求

  2. 网站的DNS域名解析器设置了CNAME,指向了g.alicdn.com,请求指向了CDN网络中的智能DNS负载均衡系统

  3. 智能DNS负载均衡系统解析域名,把对用户响应速度最快的IP节点(CDN服务器)返回给用户

  4. 用户向该IP节点(CDN服务器)发出请求

  5. 由于是第一次访问,CDN服务器会向原web站点请求,并缓存内容

  6. 请求结果发给用户

解决了哪些问题

  1. 缓解甚至消除了不同运营商之间互联的瓶颈造成的影响
  2. 减轻了各省的出口带宽压力
  3. 缓解了骨干网的压力
  4. 优化了网上热点内容的分布

性能检测工具

Devtool Performance

Lighthouse

Performance API

ref