移动互联网时代,用户对于网页的打开速度要求愈来愈高。百度用户体验部研究代表,页面放弃率和页面的打开时间关系以下图 所示。css
charthtml
根据百度用户体验部的研究结果来看,普通用户指望且可以接受的页面加载时间在 3 秒之内。若页面的加载时间过慢,用户就会失去耐心而选择离开。前端
首屏做为直面用户的第一屏,其重要性不言而喻。优化用户体验更是咱们前端开发很是须要 focus 的东西之一。html5
本文咱们经过 8 道面试题来聊聊浏览器渲染过程与性能优化。node
咱们首先带着这 8 个问题,来了解浏览器渲染过程,后面会给出题解~git
进程(process)和线程(thread)是操做系统的基本概念。github
进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。web
线程是 CPU 调度的最小单位(是创建在进程基础上的一次程序运行单位)。面试
process_threadchrome
现代操做系统都是能够同时运行多个任务的,好比:用浏览器上网的同时还能够听音乐。
对于操做系统来讲,一个任务就是一个进程,好比打开一个浏览器就是启动了一个浏览器进程,打开一个 Word 就启动了一个 Word 进程。
有些进程同时不止作一件事,好比 Word,它同时能够进行打字、拼写检查、打印等事情。在一个进程内部,要同时作多件事,就须要同时运行多个“子任务”,咱们把进程内的这些“子任务”称为线程。
因为每一个进程至少要作一件事,因此一个进程至少有一个线程。系统会给每一个进程分配独立的内存,所以进程有它独立的资源。同一进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。
借用一个生动的比喻来讲,进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,能够本身作本身的事情,也能够相互配合作同一件事情。
当咱们启动一个应用,计算机会建立一个进程,操做系统会为进程分配一部份内存,应用的全部状态都会保存在这块内存中。
应用也许还会建立多个线程来辅助工做,这些线程能够共享这部份内存中的数据。若是应用关闭,进程会被终结,操做系统会释放相关内存。
process_thread_example
一个好的程序经常被划分为几个相互独立又彼此配合的模块,浏览器也是如此。
以 Chrome 为例,它由多个进程组成,每一个进程都有本身核心的职责,它们相互配合完成浏览器的总体功能,
每一个进程中又包含多个线程,一个进程内的多个线程也会协同工做,配合完成所在进程的职责。
Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
process
因为默认 新开 一个 tab 页面 新建 一个进程,因此单个 tab 页面崩溃不会影响到整个浏览器。
一样,第三方插件崩溃也不会影响到整个浏览器。
多进程能够充分利用现代 CPU 多核的优点。
方便使用沙盒模型隔离插件等进程,提升浏览器的稳定性。
系统为浏览器新开的进程分配内存、CPU 等资源,因此内存和 CPU 的资源消耗也会更大。
不过 Chrome 在内存释放方面作的不错,基本内存都是能很快释放掉给其余程序运行的。
process_list
负责浏览器界面的显示与交互。各个页面的管理,建立和销毁其余进程。网络的资源管理、下载等。
每种类型的插件对应一个进程,仅当使用该插件时才建立。
最多只有一个,用于 3D 绘制等
称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。 (本文重点分析)
浏览器的渲染进程是多线程的,咱们来看看它有哪些主要线程 :
renderder_process
若是要讲从输入 url 到页面加载发生了什么,那怕是没完没了了…这里咱们只谈谈浏览器渲染的流程。
workflow
这是由于 Javascript 这门脚本语言诞生的使命所致!JavaScript 为处理页面中用户的交互,以及操做 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
若是 JavaScript 是多线程的方式来操做这些 UI DOM,则可能出现 UI 操做的冲突。
若是 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,
假设存在两个线程同时操做一个 DOM,一个负责修改一个负责删除,那么这个时候就须要浏览器来裁决如何生效哪一个线程的执行结果。
固然咱们能够经过锁来解决上面的问题。但为了不由于引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。
因为 JavaScript 是可操纵 DOM 的,若是在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程先后得到的元素数据就可能不一致了。
所以为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时当即被执行。
从上面咱们能够推理出,因为 GUI 渲染线程与 JavaScript 执行线程是互斥的关系,
当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。
所以若是 JS 执行的时间过长,这样就会形成页面的渲染不连贯,致使页面渲染加载阻塞的感受。
由上面浏览器渲染流程咱们能够看出 :
DOM 解析和 CSS 解析是两个并行的进程,因此 CSS 加载不会阻塞 DOM 的解析。
然而,因为 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
因此他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。
所以,CSS 加载会阻塞 Dom 的渲染。
因为 JavaScript 是可操纵 DOM 和 css 样式 的,若是在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程先后得到的元素数据就可能不一致了。
所以为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
所以,样式表会在后面的 js 执行前先加载执行完毕,因此css 会阻塞后面 js 的执行。
关键渲染路径是浏览器将 HTML CSS JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是咱们上面说的浏览器渲染流程。
为尽快完成首次渲染,咱们须要最大限度减少如下三种可变因素:
缩小、压缩以及缓存一样重要,对于 CSSOM 咱们前面重点提过了它会阻止页面呈现,所以咱们能够从这方面考虑去优化。
当浏览器遇到 script 标记时,会阻止解析器继续操做,直到 CSSOM 构建完毕,JavaScript 才会运行并继续完成 DOM 构建过程。
当浏览器碰到 script 脚本的时候 :
没有 defer 或 async,浏览器会当即加载并执行指定的脚本,“当即”指的是在渲染该 script 标签之下的文档元素以前,也就是说不等待后续载入的文档元素,读到就加载并执行。
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),可是 script.js 的执行要在全部元素解析完成以后,DOMContentLoaded 事件触发以前完成。
从实用角度来讲,首先把全部脚本都丢到 </body> 以前是最佳实践,由于对于旧浏览器来讲这是惟一的优化选择,此法可保证非脚本的其余一切元素可以以最快的速度获得加载和解析。
接着,咱们来看一张图:
defer_async
蓝色线表明网络读取,红色线表明执行时间,这俩都是针对脚本的。绿色线表明 HTML 解析。
所以,咱们能够得出结论:
来自 defer 和 async 的区别 -- nightire 回答
回流必将引发重绘,重绘不必定会引发回流。
当 Render Tree 中部分或所有元素的尺寸、结构、或某些属性发生改变时,浏览器从新渲染部分或所有文档的过程称为回流。
会致使回流的操做:
页面首次渲染
浏览器窗口大小发生改变
元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
元素字体大小变化
添加或者删除可见的 DOM 元素
激活 CSS 伪类(例如::hover)
查询某些属性或调用某些方法
一些经常使用且会致使回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft scrollIntoView()、scrollIntoViewIfNeeded() getComputedStyle() getBoundingClientRect() scrollTo()
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并从新绘制它,这个过程称为重绘。
回流比重绘的代价要更高。
有时即便仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操做进行优化:浏览器会维护一个队列,把全部引发回流和重绘的操做放入队列中,若是队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样能够把屡次回流和重绘变成一次。
当你访问如下属性或方法时,浏览器会马上清空队列:
clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft width、height getComputedStyle() getBoundingClientRect()
由于队列中可能会有影响到这些属性或方法返回值的操做,即便你但愿获取的信息与队列中操做引起的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。
渲染层合并,对于页面中 DOM 元素的绘制(Paint)是在多个层上进行的。
在每一个层上完成绘制过程以后,浏览器会将绘制的位图发送给 GPU 绘制到屏幕上,将全部层按照合理的顺序合并成一个图层,而后在屏幕上呈现。
对于有位置重叠的元素的页面,这个过程尤为重要,由于一旦图层的合并顺序出错,将会致使元素显示异常。
composite
RenderLayers 渲染层,这是负责对应 DOM 子树。
GraphicsLayers 图形层,这是负责对应 RenderLayers 子树。
RenderObjects 保持了树结构,一个 RenderObjects 知道如何绘制一个 node 的内容, 他经过向一个绘图上下文(GraphicsContext)发出必要的绘制调用来绘制 nodes。
每一个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,做为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,而后 draw 到屏幕上,此时,咱们的页面也就展示到了屏幕上。
GraphicsContext 绘图上下文的责任就是向屏幕进行像素绘制(这个过程是先把像素级的数据写入位图中,而后再显示到显示器),在 chrome 里,绘图上下文是包裹了的 Skia(chrome 本身的 2d 图形绘制库)
某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其余不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。
一旦 renderLayer 提高为了合成层就会有本身的绘图上下文,而且会开启硬件加速,有利于性能提高。
通常一个元素开启硬件加速后会变成合成层,能够独立于普通文档流中,改动后能够避免整个页面重绘,提高性能。
注意不能滥用 GPU 加速,必定要分析其实际性能表现。由于 GPU 加速建立渲染层是有代价的,每建立一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。而且在移动端 GPU 和 CPU 的带宽有限制,建立的渲染层过多时,合成也会消耗跟多的时间,随之而来的就是耗电更多,内存占用更多。过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。
这里就不细说了,有兴趣的童鞋推荐如下三篇文章 ~
Accelerated Rendering in Chrome
CSS GPU Animation: Doing It Right
从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理
若是你和我同样喜欢前端,也爱动手折腾,欢迎关注我一块儿玩耍啊~ ❤️
前端时刻
前端时刻