上一篇我们罗列了从输入url到页面渲染到底经历了什么?那我们今天再来说说这个步骤中,我们能做哪些的性能优化?
DNS Prefetch
上一篇文章只说了浏览器访问一个域名的时候需要经过DNS解析。其实DNS解析是有一个解析过程的,按浏览器缓存
、系统缓存
、路由器缓存
、ISP(运营商)DNS缓存
、根域名服务器
、顶级域名服务器
、主域名服务器
的顺序,逐步读取缓存,直到拿到IP地址。
通过主机名加载一个页面通常仅需要解析DNS一次,但是如果页面的资源文件比如:fonts、images、scripts等都是不同的主机名,DNS会对每一个进行解析。这对于性能来说是个问题,特别是对于移动网络。当一个用户用的是移动网络,每一个DNS查找必须从手机发送到信号塔,然后到达一个认证DNS服务器。手机、信号塔、域名服务器之间的距离可能是一个大的时间等待。
DNS Prefetch,即DNS预解析就是根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,来提高网站的访问速度。
如果页面之前访问了,我们可以从浏览器的DNS缓存当中直接读取。减少了解析时间,以及请求次数。
打开DNS Prefetch之后,浏览器会在空闲时间提前将这些域名转化为对应的IP地址,这里为了防止DNS Prefetch阻塞页面渲染影响用户体验,Chrome浏览器的引擎并没有使用它的网络堆栈去进行预解析,而是单独开了8个完全异步的Worker线程专门负责DNS Prefetch。所以很多人认为的DNS Prefetch会影响首屏加载其实是错误的,两者并没有任何关系,所以我们可以大胆放心的使用DNS Prefetch。
如何使用DNS Prefetch?
-
自动解析
Chromium使用超链接的
href
属性来查找要预解析的主机名。当遇到a
标签,Chromium会自动将href
中的域名解析为IP地址,这个解析过程是与用户浏览网页并行处理的。但是为了确保安全性,在HTTPS
页面中不会自动解析。<!-- https页面开启DNS Prefetch --> <meta http-equiv="x-dns-prefetch-control" content="on"> <!--http页面关闭DNS Prefetch--> <meta http-equiv="x-dns-prefetch-control" content="off">
-
手动解析
<!--xx.xx.xx表示静态资源域名--> <link rel="dns-prefetch" href="//xx.xx.xx">
HTTP“请求-响应”
- 选用高性能的web服务器,利用nginx强大的反向代理能力实现“动静分离”,实现负载均衡、增大连接池等。
- HTTP协议一定启用长链接。
- 将协议有HTTP/1 升级到 HTTP/2。因为HTTP/2 消除了应用层的队头阻塞,拥有头部压缩、二进制帧、多路复用、流量控制、服务器推送等许多新特性,大幅度提升了 HTTP 的传输效率。
- 利用缓存机制
这里重点说一下缓存。缓存又分为CDN缓存
、浏览器缓存
、本地缓存
-
CDN缓存
CDN即内容分发网络。CDN往往被用来存放像JS、CSS、图片等不需要业务服务器进行计算的资源。
-
浏览器缓存
浏览器缓存机制有四个方面:Memory Cache、Service Worker Cache、Http Cache、Push Cache。
这里重点讲一下Http Cache。
Http Cache是要结合服务端一起才能完成。在缓存数据未失效的情况下,可以直接使用缓存数据,不需要再请求服务器。Http缓存策略又细分为
强制缓存
和协商缓存
。-
强制缓存
-
Expires
响应header中添加Expires字段来标明失效规则。它的值为服务器返回的到期时间,即下一次请求时,请求时间小于服务器返回的到期时间,直接使用缓存数据。
response.setDataHeader("Expires", "0");//无缓存
-
Cache-Control(优先级高于Expires)
Cache-Control是最重要的规则。常见的取值有private、public、no-cache、max-age、no-store,默认为private。
- private:只能针对个人用户,而不能被代理服务器缓存;
- public:指示响应可被任何缓存区缓存;
- no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。
- max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
- no-store:禁止一切缓存
-
-
协商缓存
-
ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since)
服务器响应请求时,通过Etag头部告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定),浏览器再次请求时,就会带上一个头If-None-Match,这个值就是服务器上一次给的Etag的值,服务器比对一下资源当前的Etag是否跟If-None-Match一致,不一致则说明资源修改过了,浏览器不能再使用缓存,否则浏览器可以继续使用缓存,并返回304状态码。
-
Last-Modified/Last-Modified-Since
服务器响应请求时,会告诉浏览器一个告诉浏览器资源的最后修改时间:Last-Modified,浏览器之后再请求的时候,会带上一个头:If-Modified-Since,这个值就是服务器上一次给的 Last-Modified 的时间,服务器会比对资源当前最后的修改时间,如果大于If-Modified-Since,则说明资源修改过了,浏览器不能再使用缓存,否则浏览器可以继续使用缓存,并返回304状态码。
-
Http缓存的流程图如下图所示:
-
-
本地缓存
本地缓存可以使用Cookie、Local Storage、Session Storage、IndexedDB。
-
Cookie
HTTP1.0中协议是无状态的,但在WEB应用中,在多个请求之间共享会话是非常必要的,所以出现了Cookie。
第一次访问网站的时候,浏览器发出请求,服务器响应请求后,会将cookie放入到响应请求中,在浏览器第二次发请求的时候,会把cookie带过去,服务端会辨别用户身份,当然服务器也可以修改cookie内容。
特性:
- Cookie一旦创建成功,那么名字无法进行修改;
- Cookie不支持跨域,这是由Cookie隐私安全性所决定的,这样能够阻止非法获取其它网站的Cookie;
- 每个单独的域名下面的Cookie数量不能超过20个。
- 同一个域名下的所有请求,都会携带 Cookie。
- Cookie 是有体积上限的,它最大只能有 4KB。
使用方法:
// 服务端读取cookie const cookie = resquest.headers.cookie; // 服务端设置cookie response.setHeader('Set-Cookie', 'cookie1=abc;');
// 读取cookie 返回字符串 const allCookies = document.cookie; // 增加cookie newCookie是一个键值对形式的字符串。需要注意的是,用这个方法一次只能对一个cookie进行设置或更新。 document.cookie = 'cookie1=abc;';
可选的cookie属性值(用来具体化对cookie的设定/更新),使用分号分隔:
- path: 表示 cookie 影响到的路由,如 path=/。如果路径不能匹配时,浏览器则不发送这个Cookie
- httpOnly: 如果在Cookie中设置了HttpOnly属性值true,那么通过JavaScript脚本将无法读取到cookie信息,保证Cookie不会被泄露,这样能有效的防止XSS攻击;
- name: Cookie的名称,Cookie一旦创建,名称便不可更改;一个域名下绑定的cookie,name不能相同,相同的name的值会被覆盖掉。
- value: Cookie的值。如果值为Unicode字符,需要为字符编码;如果值为二进制数据,则需要使用BASE64编码;
- expires: 是一个绝对的过期时间,如果没有指定或为0表示当前会话有效;
- maxAge: 是以秒为单位的,是一个相对时间。正常情况下,max-age的优先级高于expires。Max-Age为正数时,cookie会在Max-Age秒之后,被删除,当Max-Age为负数时,表示的是临时储存,不会生出cookie文件,只会存在浏览器内存中,且只会在打开的浏览器窗口或者子窗口有效,一旦浏览器关闭,cookie就会消失,当Max-Age为0时,又会发生什么呢,删除cookie,因为cookie机制本身没有设置删除cookie,失效的cookie会被浏览器自动从内存中删除,所以,它实现的就是让cookie失效。
- domain: 可以访问该Cookie的域名,注意第一个字符必须为“.”;
- comment: Cookie的用处说明,浏览器显示Cookie信息的时候显示该说明。
- secure:当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
-
LocalStorage 是持久化的本地存储,体积一般是5MB;存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;
使用方法:
// 保存数据 localStorage.setItem("key", "value"); // 读取数据 var lastname = localStorage.getItem("key"); // 删除数据 localStorage.removeItem("key"); // 移除所有 localStorage.clear();
参考:
-
Session Storage
Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。
使用方法:
// 保存数据 sessionStorage.setItem("key", "value"); // 读取数据 var lastname = sessionStorage.getItem("key"); // 删除数据 sessionStorage.removeItem("key"); // 移除所有 sessionStorage.clear();
参考:
-
IndexedDB
IndexDB 是一个运行在浏览器上的非关系型数据库。它不仅可以存储字符串,还可以存储二进制数据。
使用方法:
-
解析渲染页面
上一篇讲到了渲染的流程。那么渲染流程中又能做哪些优化呢?
-
渲染优化
-
css阻塞
从上篇渲染流程图上就可以看出来,css解析为CSSOM和html解析为DOM是并行进行的。因为html是先解析的,往往需要等待css解析。这就造成了CSS阻塞了相关的渲染。
优化办法:
- 把css样式表全部通过
<style>
标签内联到网页中 - 将静态资源放到CDN上。
- 把css样式表全部通过
-
JS阻塞
JS引擎是独立于渲染引擎存在的。Javascript既会阻塞HTML解析,也会阻塞CSS解析。
优化方法:
- 尽量将Javascript文件放在body的底部。
- body中间尽量不要写
<script>
标签。 - 使用defer和async来避免不必要的阻塞。
-
-
重绘与回流
回流(Reflow):当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)
引发回流的操作:
- 页面首次渲染
- 浏览器窗口大小改变
- 元素的尺寸和位置发生改变
- 元素内容发生改变
- 添加或者删除DOM元素
- 激活css伪类
- 设置style属性
- 查询某些属性(offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle)或者某些方法(scrollIntoView()、scrollIntoViewIfNeeded()getBoundingClientRect()、scrollTo())
重绘(Repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
引发重绘的操作:
- color、background 相关属性(如:background-color、background-image 等)
- outline 相关属性( outline-color、outline-width )、text-decoration
- border-radius、visibility、box-shadow
重绘不一定导致回流,回流一定会导致重绘。
减少重绘与回流:
css:
- 避免使用table布局。
- 尽可能在DOM树的最末端改变class。
- 避免设置多层内联样式。
- 将动画效果应用到position属性为absolute或fixed的元素上。
- 避免使用CSS表达式(例如:calc())。
javascript:
- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
- 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
- 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
参考文献: