预处理
DNS预解析
通过DNS预解析来预先获得域名对应的ip
<link rel="dns-prefetch" href="http://baidu.com">
预加载
告知浏览器某些资源将来会被使用,将资源提前强制请求加载到本地,在需要时读取缓存,可以降低首屏的加载时间
<link rel="preload" href="http://baidu.com">
预渲染
将下载的文件预先在后台渲染
<link rel="prerender" href="http://baidu.com">
HTTP
HTTP优化
有两个方向:
- 减少请求次数
- 减少单次请求所需时间
构建工具优化
减少打包时间
-
优化loader,使用include和exclude来缩小搜索范围,并将Babel
编译过的文件缓存起来,下次只要编译更改过的代码文件即可loader: 'babel-loader?cacheDirectory=true'
-
使用DllPlugin将第三方库单独打包到一个文件中,类库不会与公共代码一起打包,只有当类库自身发生版本变化时才会重新打包。先基于 dll 专属的配置文件,打包 dll 库,再使用 DllReferencePlugin 将依赖文件引入项目中。
-
Happypack 可以将 Loader 的同步执行转换为并行的,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率。
-
使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS。在 Webpack4 中,只需要将 mode 设置为 production 就可以默认开启以上功能。
减少打包体积
- 按需加载
- 使用Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
- 使用Tree-Shaking删除冗余代码。
使用Gzip
Gzip压缩算法是HTTP压缩通常采用的压缩方案,配置为在 request headers中配置
accept-encoding:gzip
HTTP压缩是指在Web服务器和浏览器间传输压缩文本内容的方法。
Gzip的原理是临时替换代码中重复出现的字符串。
图片优化
-
使用CSS代替修饰类图片。
-
小图使用base64格式加载。
-
使用雪碧图,将小图标和背景图像合并到一张图片。然后利用 CSS 的背景定位来显示其中的每一部分。
background-position:-xpx -ypx;
-
对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式,小图使用 PNG,对于大部分图标类图片,可以使用 SVG 代替,照片使用JPEG格式。
本地存储
浏览器缓存
缓存机制
浏览器缓存有以下几个方面,按获取资源时请求的优先级如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
HTTP Cache
HTTP缓存分为强缓存与协商缓存
强缓存
在第一次请求发出后,再次发出请求时浏览器根据http 头中的 Expires 和 Cache-Control 判断是否命中缓存,若命中则返回状态码200,直接从缓存中获取资源,不会再与服务端发生通信。
Expires
该字段是http1.0时的规范,当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段。它的值为一个绝对时间的GMT格式的时间字符串,比如Expires:Mon,18 Oct 2066 23:59:59 GMT
。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。
Cache-Control
Cache-Control出现自http1.1相对于 expires 更加准确,它的优先级也高于expires 。通过设置一个时间长度来判断。 cache-control: max-age=3600
代表资源的有效时间是3600秒。
cache-control除了该字段外,还有下面几个比较常用的设置值:
- no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
协商缓存
如果缓存过期了,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
Last-Modified 和 If-Modified-Since
Last-Modified 表示本地文件最后修改日期,在首次请求时随着 Response Headers 返回:
Last-Modified: Thu,31 Dec 2018 23:59:59 GMT
随后每次请求时,会带上一个叫 If-Modified-Since 的字段,它的值正是 last-modified 的值:
服务器收到If-Modified-Since后,根据资源的最后修改时间判断是否命中缓存。如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modified。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值.
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
- 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag 。
ETag 和 If-None-Match
ETag 返回一个唯一的文件校验码,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。
如果什么缓存策略都没设置,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
缓存策略
Memory Cache
Memory Cache 也就是内存中的缓存,它的响应速度最快,是浏览器最先尝试去命中的一种缓存。但它的持续时间短,会随着渲染进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
Service Worker Cache
Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这使得 Service Worker 不会干扰页面的性能,它可以帮我们实现离线缓存、消息推送和网络代理等功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。
Push Cache
Push Cache 是 HTTP/2 中的内容,当以上缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
离线存储
本地存储分为Cookie,Local Storage,Session Storage,IndexedDB
存储的详细内容我们可以在 Chrome 的 Application 面板中查看到。
Cookie
Cookie 是由服务器生成的一个存储在浏览器里的的文本文件,它附着在 HTTP 请求上。它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。
Cookie 以键值对的形式存在。
Cookie的问题一是不够大,最大只有4kb。二是每次都会携带在 header 中,对于请求性能有影响,并且同一个域名下的所有请求,都会携带 Cookie。并且Cookie还要注意其安全性。
Local Storage
持久化的本地存储,除非被清理,否则一直存在,数据最大可以存储5m,仅位于浏览器端,不参与与服务器端的通信。适合存储内容稳定的资源。
Session Storage
临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。数据最大可以存储5m,仅位于浏览器端,不参与与服务器端的通信。即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,它们的 Session Storage 内容就无法共享。适合存储本次浏览记录等会话级的信息。
Web Storage的缺点是使用键值对,只能存储字符串,只能用于存储少量的简单数据。
IndexedDB
IndexedDB 是一个运行在浏览器上的非关系型数据库。理论上讲没有存储上限。它不仅可以存储字符串,还可以存储二进制数据。
渲染
CSS
由于CSS选择符是从右往左匹配的,所以有以下方法:
- 避免使用通配符
* {}
,只对需要用到的元素进行选择。 - 关注可以通过继承实现的属性,避免重复匹配重复定义。
- 少用标签选择器
#
。如果可以,用类选择器.
替代 - id 和 class 选择器不要使用多余的标签选择器。
- 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。
此外,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。因此需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。如尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)
JS
JS 引擎是独立于渲染引擎存在的。为了防止JS对DOM进行修改,所以当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。所以假如我们可以确认一个 JS 文件的执行时机并不一定非要是此时此刻,我们就可以通过对它使用 defer 和 async 来避免不必要的阻塞。
async:
<script async src="index.js"></script>
async 模式下,异步下载脚本文件,下载完毕立即解释执行代码。
defer:
<script defer src="index.js"></script>
defer模式下,异步下载脚本文件,脚本会在文档解析完成后,DOMContentLoaded 事件即将被触发时依次执行。
一般脚本不依赖于DOM 元素和其它脚本之时,选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,选用 defer。
DOM优化
由于JS 引擎和渲染引擎(浏览器内核)是独立实现的,当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间通过桥接接口进行通信。我们每操作一次 DOM,都要通过一次桥接接口,次数一多就会产生比较明显的性能问题。
而当我们对 DOM 的修改会引发它外观(样式)上的改变时,就会触发回流或重绘。
- 回流:当我们对 DOM 的修改引发了 DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
- 重绘:当我们对 DOM的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
引起回流的原因:
- 改变 DOM 元素的几何属性(width、height、padding、margin、left、top、border 等)。
- 改变 DOM 树的结构(节点的增减、移动等操作)
- 获取一些特定属性的值:如offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight时。这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会提前将 flush 队列的任务出队,引发回流。
重绘不一定导致回流,回流一定会导致重绘。我们在开发中,要尽可能减少回流和重绘的次数
优化方法
- 使用变量存放DOM节点,防止DOM节点的反复访问
- 整合JS操作,处理完事以后再一起更新到DOM上。如使用DocumentFragment进行缓存操作。DocumentFragment对象允许我们像操作真实DOM一样,最后append 进真实 DOM,只引发一次回流和重绘。或使用display:none,先隐藏在修改再重新显示,只引发两次回流和重绘。或使用cloneNode和replaceChild技术,引发一次回流和重绘。
- 将那些改变样式的操作集合在一次,直接改变className或者cssText。
- 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。
- 不要使用table 布局,因为table 的每一个行甚至每一个单元格的样式更新都会导致整个table 重新布局
- 用transform 代替 top,left ,margin-top, margin-left… 这些位移属性
- 不要使用 js 代码对dom 元素设置多条样式,选择用一个 className 代替之。
- 动画的速度按照业务按需决定
- 对于频繁变化的元素应该为其加一个 transform 属性,对于视频使用video 标签
- 减少不必要的DOM深度,去除没有用处的css,避免不必要的复杂的css选择符
- 页面的元素适当定高,例如如果div内容可能有高度差异的动态内容载入,页面刷新载入的时候,应避免页面元素的晃动、位移等,这些都是额外的重绘。
- 不要经常访问会引起浏览器flush队列的属性,非要高频访问的话建议缓存到变量。
异步更新
当我们使用 Vue 或 React 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。
异步更新的特性在于它只看结果,因此渲染引擎不需要为过程买单。
页面渲染
首屏加载
懒加载
用户点开页面的瞬间,呈现给他的只有屏幕的一部分。等用户下拉的瞬间再即时去请求、即时呈现下面的图片给他。
对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,
<div data-src="https://..." style="background-image: none; background-size: cover;" ></div>
当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。
style="background-image: url(https://...); background-size: cover;"
通过循环图片列表,比对可视区域高度和元素顶部距离可视区域顶部的高度来判断是否有图片元素露出,如有元素露出加载图片,从下一张图片开始检查是否露出。
防抖与节流
防抖
举个例子,有一个按钮点击会触发网络请求,但是我们并不希望每次点击都发起网络请求,而是当用户点击按钮一段时间后没有再次点击的情况才去发起网络请求。因此在一段时间内,不管触发了多少次回调,我们都只认最后一次。
节流
举个例子,滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,因此在一段时间内,不管触发了多少次回调,我们都只认第一次,并在计时结束时给予响应。
性能监控
Performance
选中实心圆按钮,Performance 会开始记录我们后续的交互操作;选中圆箭头按钮,Performance 会将页面重新加载,计算加载过程中的性能表现。
我们可以详细的看到每个时间段中浏览器在处理什么事情,哪个过程最消耗时间,便于我们更加详细的了解性能瓶颈。
Audits
点击 Run audits ,工具就会自动运行帮助我们测试问题并且给出一个完整的报告
报告中分别为性能、体验、SEO 都给出了打分,并且往下拉每一个指标都有详细的评估。
Performance API
在 performance 的 timing 属性中,我们可以查看到如下的时间戳:
这些时间戳与页面整个加载流程中的关键时间节点有着一一对应的关系:
参考文章:https://juejin.im/book/5b936540f265da0a9624b04b
https://juejin.im/book/5bdc715fe51d454e755f75ef