浏览器进程
Browser进程
- 浏览器的主进程(负责协调、主控),只有一个
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将渲染进程(Render)得到的内存中的位图(Bitmap), 绘制到用户界面上
- 网络资源的下载,管理等
第三方插件进程
- 每种类型的插件对应一个进程,仅当使用该插件时才创建
GPU进程
- 最多一个,用于3D绘制
浏览器渲染进程
- 浏览器内核(render进程)
- 默认每个Tab页面一个进程,互不影响
- 页面渲染,脚本执行,事件处理
渲染进程中的线程
GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树,和Render树,布局和绘制
- 界面触发了重绘和回流时,该线程就会执行
- GUI线程与JS线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了), GUI线程会被保存在一个队列中等到JS引擎空闲时立即被执行
JS引擎线程
- 也被称为JS内核,负责处理JavaScript脚本程序(例如v8引擎)
- 负责解析JavsScript脚本,运行代码
- JS引擎一直等待着任务队列中任务的到来,然后处理,一个Tab页(Render进程)任何时候都只有一个JS线程在运行JS程序
- 当对应的时间符合条件被触发的时候,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- GUI线程和JS引擎线程是互斥的,如果JS执行的时间比较长,会造成页面的渲染不连贯,页面加载阻塞
定时触发器线程
- setTimeout与setInterval所在的线程
- 浏览器定时器计数器并不是由JavaScript引擎计数的
- setTimeout中低于4ms的时间间隔算为4ms
事件触发线程
- 归属浏览器而不是JS引擎,用来控制事件循环
- 当JS引擎执行代码块如setTimeout时,(或鼠标事件,AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到带处理队列的队尾,等待JS引擎的处理
异步http请求线程
- 在XMLHttpRequest连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,由JS引擎执行
Browser进程和渲染进程的通信
- Browser进程收到用户请求,首先获取页面内容(比如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
- Render进程接口收到消息,简单解释后,交给渲染线程,然后开始渲染
- 渲染线程收到请求,加载网页并渲染网页,其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
- 可能会有JS线程操作DOM(可能会造成回流并重绘)
- 最后Render进程将结果传递给Browser进程
浏览器渲染过程
- 输入url,Browser进程接管,开启一个下载线程
- 可能命中缓存或者没有命中缓存通过http请求,拿到资源
- 渲染进程接管,生成DOM树、css树,并结合生成render树,绘制页面像素信息
- 浏览器将各层的信息发送给GPU,GPU会将各层合成(composite), 显示在屏幕上
浏览器缓存
浏览器发请求获取资源的时候,会先进行缓存查找,如果有可用的缓存,那么就直接从缓存中读取资源,否则再从服务器中请求获取。
前端页面和资源是否被浏览器缓存,一般是由服务器通过设置http响应头部告诉浏览器的,HTTP/1.0中的Expires和Last-Modified,Http/1.1中的cache-control和ETag.
- 发起请求
- 是否命中强缓存,如果命中则直接从缓存中读取资源,否则下一步
- 强缓存没有命中的时候,会发一个请求到服务器,服务器根据请求头中的部分字段,判断是否命中协商缓存,如果命中,则返回304,否则,返回资源
强缓存
- 发请求时,浏览器会先检查请求头中的expires和cache-control字段
- expires:
该字段是 http1.0 时的规范,它的值为一个绝对时间的 GMT 格式的时间字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。
- cache-control:
Cache-Control 是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间,例如 Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒。cache-control 除了该字段外,还有下面几个比较常用的设置值:
no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
no-store:禁止使用缓存,每一次都要重新请求数据。
public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。
Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。
协商缓存
协商缓存中用到的header中的信息指的是 Last-Modify/If-Modify-Since 和 ETag/If-None-Match.
Last-Modified/If-modified-Since
- 浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify, Last-Modify是一个时间,标志该资源的最后修改时间
- 当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since, 该值为缓存之返回的Last-Modify,服务器收到If-Modify-Since后,会根据资源的最后修改时间判断是否命中
- 如果命中,返回304,不会返回资源内容,并且不会返回Last-Modify
- 缺点:短时间内资源发生了变化,Last-Modify并不会发生变化
- 如果在一个周期内,资源修改会原来的样子,资源认为是可缓存的,但是Last-Modify会重新返回 ETag/If-None-Match
- 服务器会优先验证Etag,一致的情况下,才会继续比对Last-Modify,最后决定是否返回304*
- ETag是服务器响应请求时,返回当前资源的唯一标志
- If-None-Match是浏览器请求时,带上的上一次服务器返回的ETag的值
- 服务器收到If-None-Match后,会和资源在服务器的ETag的值作对比,一致则返回304,不一致就返回资源
memory cache
- 目前webkit资源分成两种,一种是主资源(比如HTML页面,或者下载项),一种是派生资源(比如页面内嵌的图片,脚本链接)
- memory cache就是将资源缓存到内存中,下次访问时不需要重新下载资源,直接从内存中获取
- memory cache只针对派生资源,用于保存原始数据,比如css,js,图片
- 一般放脚本,字体,图片,进程退出就被清除
disk cache
- 将资源存在磁盘中
- 一般放css等,进程退出不会被清理
启发式缓存
- 如果一个可以缓存的请求没有设置Expires和cache-control,但是响应头中有设置Last-Modified信息,这种情况下浏览器有一个默认的缓存,即缓存时间为:(Date - Last-Modified) * 10%
三级缓存原理
- 先在内存中查找,有就直接加载
- 内存中没有,就在硬盘中查找,有就直接加载
- 如果硬盘中也没有,就进行网络请求
- 拿到资源后缓存到硬盘和内存中
具体渲染过程
- HTML被解析器解析成DOM树
- CSS被CSS解析器解析成CSS树
- 结合DOM树和CSS树,生成Render树
- 生成布局(flow), 将渲染树的所有节点进行平面合成
- 将布局绘制(paint)在屏幕上
- JS引擎执行栈内代码
- 微任务
- 浏览器闲置
- GUI线程渲染
回流
布局或几何属性发生改变,对应上面的第4步
会导致回流的操作:
- 页面的首次渲染
- 浏览器窗口发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或者图片大小等)
- 元素字体发生变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(:hover)
- 查询某些属性,或者调用某些方法 常用的会导致回流的属性和方法
- clientWidth, clientHeight, clientTop, clientLeft
- offsetWidth, offsetHeight, offsetTop, offsetLeft
- scrollWidth, scrollHeight, scrollTop, scrollLeft
- scrollIntoView(), scrollIntoViewIfNeeded()
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
重绘
需要更改外观而不会影响布局的, 对应上面的第5步
- color, background-color, visibility
重绘回流带来的性能影响
发生回流必定发生重绘 现在浏览器会维护一个队列,把所有引起重绘和回流的操作放入队列中,如果队列中任务数量或者时间间隔达到一个阈值,浏览器就会将队列清空,进行一次批处理,把多个重绘回流变成一次。 当访问一下属性时,浏览器会立刻清空队列
- clientWidth, clientHeight, clientTop, clientLeft
- offsetWidth, offsetHeight, offsetTop, offsetLeft
- scrollWidth, scrollHeight, scrollTop, scrollLeft
- width, height
- geteComputedStyle()
- getBoundingClientRect() 以保证你拿到的是最精确的值
优化
- 避免使用table布局,将动画效果应用到position属性为absolute或者fixed的元素上
- 避免使用css表达式calc()
- 避免频繁操作DOM,创建一个documentFragment, 在它上面应用所有Dom操作,最后添加到文档中
- 对display: none的元素进行操作不会引起重绘和回流
垃圾回收机制
JavaScript执行的时候会创建很多变量,这些变量不再需要的时候,就称之为垃圾,释放无用变量所占用内存的过程,叫做垃圾清理(Garbage Collection)。最常见的两种垃圾回收算法是标记清除和引用计数。谷歌浏览器的V8引擎引入了新老代机制,在标记清除的基础上还做了很多优化。
两种垃圾回收策略
标记清除
标记清除是目前最常用的,此算法分为标记和清除两个阶段 标记阶段:
- 垃圾收集器运行时,给每个变量都加上一个标记,全都记为0
- 从各个根对象开始遍历,吧不是垃圾的节点改为1 清除阶段:
- 清理所有标记为0的垃圾,销毁并回收他们所占用的内存空间
- 最后,把所有内存中的对象标记修改为0, 等待下一轮垃圾回收
引用计数
引用计数是早先的垃圾回收算法,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候,这个值的引用次数就为1
- 如果同一个值又被赋值给另一个变量,那么引用次数加1
- 如果该变量的值被其他的值覆盖了,则引用次数减1
- 当这个值的引用次数变为0的时候,说明没有变量在使用,这个值无法被访问,回收空间,垃圾回收器会在运行的时候清理掉引用次数为0的值占用的内存
- 最大的缺点是,当两个对象互相引用的时候,两个对象的引用数量始终都是2,无法被回收,这样的变量多了之后就会造成大量的内存不会被释放
V8的垃圾回收机制
分代式垃圾回收
V8的垃圾回收策略主要基于分代式垃圾回收机制,V8中将堆内存分为新生代和老生代两区域。采用不同的垃圾回收器回收垃圾。
新生代
新生代的对象为存活时间较短的对象,通常只支持1-8M的内存
- 新生代的内存一分为二,一个是处于使用状态的空间使用区,一个是处于闲置状态的空间空闲区。
- 新加入的对象会存到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作
- 当开始垃圾回收时,新生代垃圾回收器会对使用区的活动对象做标记,标记完成之后将使用取的活动对象复制进空闲区并排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉,最后空闲区和使用区的位置互换。
- 如果一个对象经过多次复制后依然存活,它将被认为是生命周期较长的对象,会被移动到老生代中,
- 如果一个对象复制到空闲区时,空闲区空间占用超过25%, 那么这个对象会被直接晋升到老生代中。
老生代
老生代就是采用了标记清理进行垃圾回收
V8的垃圾回收机制的优化
标记整理
标记清除算法的缺点是,清除后,剩余对象的内存时不变的,会导致空闲内存空间不连续,出现内存碎片,会降低内存的使用效率。标记整理算法可以解决这种问题
- 在标记阶段结束后,标记整理算法会将活着的对象向内存的一端移动,最后清理掉边界的内存 V8采用了标记整理来解决标记清除带来的内存效率问题来优化空间
并行回收
JavaScript是单线程语言,进行垃圾回收的时候,会阻塞JavaScript脚本的执行,要等待回收完毕后再恢复执行,这一现象称之为全停顿。
为了提高垃圾回收的执行速度,V8引入了并行回收机制。
新生代对象空间执行垃圾回收的时候,会启动多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域。
增量标记
对于老生代垃圾回收来说,内部存放的都是交大的对象,即使采用并行策略也依然会消耗大量时间。 增量标记就是讲一次标记的过程分成很多小步,执行完一小步就让应用逻辑执行一会,多次交替完成一轮标记。对象被标记后又发生了改变时,会出现问题,这里又引入了三色标记法
三色标记法
- 未标记的对象标记为白色
- 自身被标记,成员变量没有被标记的记为灰色
- 自身和成员皆被标记的记为黑色
- 一旦有黑色对象引用了白色对象,白色对象会被改为灰色对象
- 最初所有的对象都是白色的,从根对象开始,先将这组根对象标记为灰色并推入到标记工作表中,当访问到他的引用对象时,他本身变为黑色,引用对象变为灰色,一直往下走,直到没有可标记的灰色对象时,剩下所有的白色对象都是无法到达的,等待回收。
惰性清理
清理阶段,当前的内存如果足以支持快色的执行代码,那么久没有必要立即清理内存,可以吧清理过程延迟一下,也无需一次性清理所有非活动对象,可以按序逐一清理。
并发回收
主线程在执行JavaScript的过程中,辅助线程仍然能在后台完成执行垃圾回收的操作
参考文章
# 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理