在前端面试中,有一道经典的面试题:请描述一下从url输入到页面渲染的过程。这道题很考察候选人的能力和知识深度,
今天我们以这个问题为基础,讲一下在此过程中能做哪些优化。
从输入 URL 到页面加载完成的过程
首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
http优化
增加安全性:用https代替http
http详解
http是超文本传输协议,但是在信息传输过程中,由于通信使用的是明文,可能存在信息被窃取和篡改的问题。
可以使用安全的https替代http
详见Carlo大佬的博文 细说https
减少请求次数:浏览器缓存策略
当发送http请求时,浏览器首先会按顺序检查以下四项是否存在缓存
Memory Cache
Service Worker Cache
HTTP Cache
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
它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
Push Cachae
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。
Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
关于浏览器的强缓存和协商缓存,详见Carry的博文 前端静态资源缓存
关于sevice Worker缓存, 详见Carry的博文 前端静态资源缓存之sevice worker
减少单次请求所花费的时间: CDN
CDN 的核心点有两个,一个是缓存,一个是回源。
“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN 往往被用来存放静态资源。所谓“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。而“动态资源”,顾名思义是需要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染得到的 HTML 页面。
那“非纯静态资源”呢?它是指需要服务器在页面之外作额外计算的 HTML 页面。具体来说,当我打开某一网站之前,该网站需要通过权限认证等一系列手段确认我的身份、进而决定是否要把 HTML 页面呈现给我。这种情况下 HTML 确实是静态的,但它和业务服务器的操作耦合,我们把它丢到CDN 上显然是不合适的。
另外,CDN的域名必须和主业务服务器的域名不一样,要不,同一个域名下面的Cookie各处跑,浪费了性能流量的开销,CDN域名放在不同的域名下,可以完美地避免了不必要的 Cookie 的出现!
渲染优化
浏览器的渲染机制一般分为以下几个步骤:
处理 HTML 并构建 DOM 树。
处理 CSS 构建 CSSOM 树
将 DOM 与 CSSOM 合并成一个渲染树。
根据渲染树来布局,计算每个节点的位置。
调用 GPU 绘制,合成图层,显示在屏幕上
在渲染dom的时候,浏览器实际上所做的工作是
获取DOM后分割为多个图层
对每个图层的节点计算样式结果(Recalculate style–样式重计算)
为每个节点生成图形和位置(Layout–回流和重布局)
将每个节点绘制填充到图层位图中(Paint Setup和Paint–重绘)
图层作为纹理上传至GPU
复合多个图层到页面上生成最终屏幕图像(Composite Layers–图层重组)
服务端渲染
css优化建议
CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK。我们将 CSS 放在 head 标签里 和尽快 启用 CDN 实现静态资源加载速度的优化。
CSS 选择符是从右到左进行匹配的,比如 #myList li {}实际开销相当高。
避免使用通配符,只对需要用到的元素进行选择。
关注可以通过继承实现的属性,避免重复匹配重复定义。
少用标签选择器。如果可以,用类选择器替代。错误:#dataList li{} 正确:.dataList{}
不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿。错误:.dataList#title 正确:#title
减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。
js阻塞和Event Loop
JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。
JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。
我们将耗时的js操作放在页面的尾部,等待html和css渲染完毕再执行js防止js阻塞页面渲染。
js 宏任务和微任务
JS中分为同步任务和异步任务,同步任务在执行栈中执行,异步任务的回调被放在任务队列中,等待执行栈空闲时执行。
setTimeout/setInterval,XHR/fetch的回调函数都属于异步任务。任务栈执行一次的过程属于一次宏任务,在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。
微任务是在一次宏任务执行完毕之后,GUI线程执行之前执行的任务。当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完.
Promise,process.nextTick等,属于微任务
合理的安排宏任务微任务以及dom操作的顺序,可以尽可能的减少无效的dom操作。
更多关于js和Event Loop的知识,请参考云中桥大佬的博文 从多线程到Event Loop全面梳理
回流和重绘
回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
重绘不一定导致回流,回流一定会导致重绘。回流比重绘做的事情更多,带来的开销也更大。在开发中,要从代码层面出发,尽可能把回流和重绘的次数最小化。