本文讲解浏览器基本原理, 以Chrome为例, 概览浏览器全貌, 了解浏览器背后的工作机制, 为更深入的前端开发作准备.
如何区分进程和线程
进程有自己独立的资源,包括内存、堆栈等, 进程之间相互独立. 线程属于进程, 一个进程中至少包含一个线程, 一个进程中的多个线程共享线程的资源.
在电脑中可以打开任务管理器( mac 中叫活动监视器 ),, 可以看到一个进程列表. 同时还能看到进程对系统资源的占用情况
image.png
或者使用命令行工具htop查看
image.png
进程是CPU资源分配的最小单位.线程是CPU调度的最小单位, 线程是建立在进程基础上的一次程序运行单位.
不同进程之间也可以通信, 不过代价比较大.11
现在通常所说的单线程和多线程,都是指的一个进程内的.
浏览器是多进程的, 通常来说一个页面对应这一个进程
浏览器之所以能够运行, 是因为操作系统给它提供了所需要的资源, 如内存、 CPU等
浏览器中也包含一个类似于任务管理器的工具, 在Chrome中可以通过菜单->更多工具->任务管理器打开.
image.png
在Chrome中, 每个Tab页面都有一个独立的进程, 另外浏览器还有一个主进程, Browser进程, 但浏览器有自己的优化机制, 有些进程会被合并, 比如多个新Tab页面会被合并到一个进程中, 插件的后台和前台会合并在一个进程中等.
相比于单进程浏览器,多进程浏览器
但多进程的浏览器内存资源消耗比较大. 现代浏览器已实现自动的进程管理, 会根据不同的的及其性能环境实现部分进程的合并, 以减少资源的消耗
浏览器内核进程也是Renderer进程.
可以认为页面的渲染、JS的运行、事件循环都在这个进程内
浏览器的渲染进程是多线程的, 内核进程中通常包含一下线程:
当我们打开一个页面时, Brewser进程收到我们的请求, 首先需要获取页面的内容, 随后将该任务通过RendererHost接口传递给Render进程.
Renderer进程收到接口消息后, 进行简单解释, 然后交给渲染进程, 之后开始渲染
Browser进程接收到结果后将页面显示出来
由于JavaScript可以操作DOM, 如果在修改元素的同时渲染页面, 那么渲染前后元素的数据可能不一致. 为了防止渲染出现不可预期的结果, 浏览器设置了GUI和Js引擎互斥的关系, 当其中一个执行的时候, 另外一个会被挂起.
当JS长时间执行的时候, 由于GUI线程被挂起, 此时就算GUi有更新,也会被保存在队列中,等待JS空闲后再渲染. 如果JS由于大量的计算, 需要很长时间才能空闲的话, GUI就会长时间不能更新页面, 自然就感觉页面卡顿. 因此要避免JS长时间占用, 导致页面卡顿.
React 在更新过程中会有大量的计算, 因此React在16版本之后使用了异步渲染的方式, 让更新过程可以暂停, 使得GUI能有机会更新页面, 从而防止页面卡顿.
为了让JS能应对CPU密集型计算任务, 在HTML5中加入了WebWorker.
Web Worker 为内容在后台线程中运行脚本提供了一种简单的方法. 线程可以执行任务而不干扰用户界面. 此外也可以使用网络请求执行I/O. 一旦创建, 一个worker可以将消息发送到它的父线程, 反之亦然.
worker运行在另一个全局上下文中, 不同于主线程的window, 因此在worker中使用window将会返回错误
创建worker时, JS引擎向浏览器申请新开一个子线程( 子线程是浏览器开的, 完全受主线程控制, 并且不能操作dom ).
JS引擎线程与worker线程间通过特定的方式通信, 需要序列化对象来与线程进行特定数据的交互.
虽然我们可以通过web worker 来开启新的线程, 但JS引擎的单线程本质并没有改变.
区别
WebWorker只属于某个页面, 不会喝其他页面的进程共享
SharedWorker是所有页面共享的, 属于浏览器进程
浏览器内核拿到请求的内容后, 大致可以划分为几个步骤
渲染完成后就是触发load事件, 执行js脚本
image.png
当DOM加载完成, 会触发DOMCOntentLoaded事件, 不包括样式表 图片等.
当页面上的所有的DOM、 样式、 脚本、图片都加载完成时, 会触发load事件
DOMContentLoaded事件一定早于onload吗???
css加载不会阻塞DOM树的解析, 加载过程中DOM正常构建, 但会阻塞render树的渲染, 因为render树的构建是依赖css.
如果不阻塞的话, 当css加载完成后,render树可能需要需要重绘甚至回流, 造成没有必要的损耗.
浏览器渲染的图层包含两类, 普通图层和复合图层
普通文档流可以理解为一个复合图层(默认复合层), 使用absolute 、fixed定位的元素通常也在这个图层里.
当我们使用硬件加速的方式声明一个新的复合图层时, 这个图层后单独分配资源, 这个图层里面引起的回流重绘不会影响默认复合层.
可以认为: GPU中, 各个图层是单独绘制的, 所以互不影响. 这也是为什么某些场景下硬件加速的效果非常好
可以在Chrome的调试工具中找到图层工具, 查看页面元素的图层信息
image.png
硬件加速
将该元素变成一个复合图层, 就是传说的硬件加速技术
absolute和硬件加速的区别
absolute虽然可以脱离普通文档流, 当没有脱离默认复合层
所以, 即使absolute的信息改变不会改变普通文档流中render树, 但是, 浏览器最终绘制时, 是整个复合层绘制的, 而硬件加速就直接在另一个复合层绘制, 因此这个复合层的绘制不会影响默认复合层, 仅仅引起最后的合成.
复合图层的作用
一般一个元素开启硬件加速后会变成复合图层, 可以独立于普通文档流中, 改变后可以避免整个页面的重绘, 提升性能.
但是尽量不要大量使用复合图层, 否则由于资源消耗过渡, 页面反而会变得更卡.
硬件加速时请使用index
使用硬件加速时, 尽可能使用index, 防止浏览器默认给后续元素创建复合层渲染
如果这个元素添加了硬件加速, 并且index层级比较低, 那么这个元素的后面其他元素, 会变成复合层渲染, 如果处理不当会引起极大的性能问题.
简单来说, 如果A是一个复合图层, 并且B在A上面, 那么B也会被隐式转为一个复合图层.
JS分为同步任务和异步任务
同步任务都在主线程上执行, 形成一个执行栈
主线程之外, 事件触发线程管理这一个任务对你了,只要异步任务有了运行结果, 就在任务队列字中放置一个事件. 一旦执行栈中所有的同步任务执行完毕, 系统就会读取任务队列, 将可运行的异步任务添加到执行栈中, 开始执行.
这也可以解释了为什么浏览器定时器是有误差的了, 因为定时器结束时只是将任务推送到事件队列中, 此时的执行栈还有别的任务, 因此需要等到这些任务完成之后, 才会开始执行定时器的任务.
主线程执行时会产生执行栈, 栈中的代码在产生异步任务时, 会在任务队列中添加各种事件, 如定时器, 网络请求等
栈中的代码执行完毕时, 会从任务队列中获取任务来执行,继而产生循环.
事件循环机制的核心是Js引擎线程和事件触发线程
定时器是有定时器线程控制的, 当调用setTimeout或setInterval时, 计时器就开始计时, 计时完成后会将指定的回调添加到任务队列中, 等待主线程执行.
用setTimeout模拟定期计时和直接使用setInterval是有区别的, 因为每次setTimeout计时结束后会执行代码, 然后才会设定新的计时器. 中间会有误差. 而setInterval则每次都会在精确的之后推入事件, 当事件的执行就不一定准确了, 有可能上一个事件还没执行, 下一个事件就来了. 这就是setInterval的累积效应, 会导致代码执行很多次, 而且之间没有间隔.
因此, 一般会建议使用setTimeout模拟setInterval, 或者在做动画时使用requestAnimationFrame.
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
宏任务: 可以理解是每次执行栈的代码就是一个宏任务. 每一个task会从头到尾执行, 不会执行其他的代码. 浏览器为了能够使得JS内部task与DOM任务能有序的执行,会在一个task执行结束后, 下一个 task 执行之前, 对页面进行想重新渲染.
常见的宏任务有setTimeout、 setInterval、MessageChannel, 并且MessageChannel的优先级大于setTimeout
宏任务是由事件线程维护
微任务: 可以理解为在当前task结束后立即执行的任务, 也就是说, 在当前task任务后, 下一个task之前执行, 因此它比setTimeout会更快, 因为无序等待渲染.
常见的微任务有Promise、process.nextTick ( 在node环境下, process.nextTick的优先级会高于Promise, 先于Promise执行 )
微任务由JS引擎维护
官方的Promise是micro task, polyfill版本是宏任务, 是通过setTimeout模拟的
image.png
浏览器执行过程
领取专属 10元无门槛券
私享最新 技术干货