浏览器原理
进程:启动程序时,操作系统会创建一块内存供程序使用,这就是进程。
线程:进程内部内存分配最小单元,进程包含多个线程,提高并行处理效率。
- 进程之间完全隔离,某个进程崩溃不会影响其他进程。
- 进程中并发多个线程,负责执行不同任务。
- 线程共享进程中的数据,互相可以直接通信,任意一个线程出错,进程就会崩溃。
- 进程间通信需要特殊方式。
进程间通信(IPC)方式:
- 管道通信:就是操作系统在内核中开辟一段缓冲区,进程1可以将需要交互的数据拷贝到这个缓冲区里,进程2就可以读取了
- 消息队列通信:消息队列就是用户可以添加和读取消息的列表,消息队列里提供了一种从一个进程向另一个进程发送数据块的方法,不过和管道通信一样每个数据块有最大长度限制
- 共享内存通信:就是映射一段能被其他进程访问的内存,由一个进程创建,但多个进程都可以访问,共享进程是最快的IPC方式
- 信号量通信:比如信号量初始值是1,进程1来访问一块内存的时候,就把信号量设为0,然后进程2也来访问的时候看到信号量为0,就知道有其他进程在访问了,就不访问了
- socket:其他的都是同一台主机之间的进程通信,而在不同主机的进程通信就要用到socket的通信方式了,比如发起http请求,服务器返回数据
早起浏览器是单进程的,网络,JS运行,渲染等都在一个进程中,这会导致:
- 不稳定,标签页卡死,可能导致整个浏览器崩溃。
- 不安全,由于同一进程内的线程可以共享数据,JS线程就可以随意获取浏览器数据,可能导致恶意攻击。
- 性能差,一个进程需要负责太多事情,会导致运行效率问题
现代V8浏览器打开一个页面至少包括:浏览器进程、渲染进程、GPU进程、网络进程。如果浏览器使用了插件,还会包含插件进程。
- 浏览器进程: 负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能
- GPU进程:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程
- 网络进程:负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程
- 插件进程:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响
- 渲染进程:负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎Blink和JS引擎V8都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程
在渲染进程中又包含多个线程:
- GUI渲染线程:负责渲染页面,解析html和CSS、构建DOM树、CSSOM树、渲染树、和绘制页面,重绘重排也是在该线程执行
- JS引擎线程:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS。它GUI渲染进程不能同时执行,只能一个一个来,如果JS执行过长就会导致阻塞掉帧
- 计时器线程:指setInterval和setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作
- 异步http请求线程: XMLHttpRequest连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲执行
- 事件触发线程:主要用来控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理。
- 合成线程:负责生成视口附近的图块,为光栅化生成位图做准备。
用户在输入URL到页面展示发生了什么
- 浏览器主进程检测用户输入内容,如果是域名,则会补齐协议。如果是不符合域名格式,则会使用浏览器默认搜索引擎进行搜索。页面跳转之前,还会在当前页面调用一次
beforeunload
事件,数据清理、确认离开。 - 浏览器主进程通过进程间通信 IPC 来将url请求交给网络进程。由网络进程发起请求。
- 网络进程会查找该请求是否在本地有缓存,如果有则使用缓存,如果没有则准备发送网络请求。(这里缓存指强缓存,如果强缓存过期,会进入协商缓存,仍会发送请求)
- 请求首先要去 DNS(域名和IP地址映射服务) 上获取IP地址,建立TCP连接,构建请求信息并添加cookie到请求头中,然后发送HTTP请求
- 服务器接收到请求后,将响应数据发送回网络进程。
- 网络进程解析响应头中的字段(跨域拦截)并传递给主进程。如果状态码是301或302,表明地址需要重定向,此时读取响应头中
Location
字段中的重定向URL,然后重新发起一遍流程。 - 状态码为200:可以正常处理响应数据。此时浏览器主进程根据
Content-Type
字段的类型来判断如何处理响应数据。如果是 text/html 格式,则准备渲染进程。 - 准备渲染进程:并不是每个页面都会分配一个新的渲染进程,如果两个页面是同一根域名下,就可能使用同一渲染进程。在这里还会进行一次判断。
- 提交文档:
- 浏览器进程接受到网络进程的响应头数据,向渲染进程发送『提交文档』消息;
- 渲染进程收到『提交文档』消息后,与网络进程建立传输数据『管道』,传递html文本数据。
- 传输完成后,渲染进程返回『确认提交』消息给浏览器进程;
- 浏览器接受『确认提交』消息后,移除旧文档、更新界面、地址栏,导航历史状态等,浏览器进入渲染阶段。
- 渲染阶段
- 将 HTML 文本转化为 DOM 树结构(边接收html文本数据,边解析生成DOM,与文档确认提交消息发送没有明确先后) 。
- 将 CSS 文本转化为 styleSheets。
- 根据 CSS 的继承和层叠规则,计算每个DOM节点的样式
- 布局:构建一棵只包含可见元素布局树,计算每个元素的几何位置,并保存在布局树中。
- 分层
- 图层绘制
- 栅格化,渲染进程中的合成线程将图层划分为图块,并选择视口附近的图块,通过栅格化(借助GPU)转换为位图。生成的位图被保存在 GPU 内存中。
- 合成:栅格化完成后,合成线程会生成
DrawQuad
命令,提交给浏览器进程,并显示到屏幕上。
重排,重绘,合成
- 重排:通过 js 或 css 修改元素几何位置属性,此时浏览器会触发重新布局(渲染阶段第四步),需要更新的步骤最多,性能开销最大。
- 重绘:修改元素外观,不影响几何属性,省去了布局和分层阶段。
- 合成:既不修改外观,也不修改几何属性,比如 CSS3 的 transform ,还可以省去图层绘制阶段。直接在非主线程上合成,效率更高。
V8执行与垃圾回收
编译型语言:在运行时会通过编译器编译出机器能识别的二进制文件。再次运行时可以直接运行二进制文件。
解释型语言:每次运行都需要通过解释器对代码进行动态解释执行。
JS是解释型语言。
V8执行JS过程
- 将源代码通过词法分析和语法分析转换为抽象语法树AST并生成执行上下文
- 解释器
Ignition
根据抽象语法树 AST 生成字节码 - 字节码生成后进入执行阶段, 解释器
Ignition
会将字节码逐条解释执行。在这个过程中,如果解释器发现热点代码 ,后台的编译器TurboFan
就会将这段热点代码编译为机器码保存,再次执行时提高了效率(即时编译JIT)
- 抽象语法树AST 是一种数据结构,能够被编译器和解释器识别。Babel就是通过将ES6的AST转换为ES5的AST来实现的转译代码。
- 词法分析 又叫做分词,将代码拆分成不可分割的最小字符串(token)
- 语法分析 又叫做解析,将生成的字符串根据语法规则转换为AST。如果不符合规则会抛出语法错误。
- 字节码 介于AST和机器码之间的一种代码。无法直接识别,但是占用内存较机器码更少。
- 热点代码 执行多次的代码
- 及时编译JIT 指使用字节码配合解释器和编译器来优化提效的方案
V8垃圾回收
对于保存在栈中的数据,JS通过一个指针ESP来判断当前执行位置,当函数执行完毕以后,该指针也会移动到栈中的下一个执行上下文,之前函数的上下文环境也就无效了,如果有新的函数上下文入栈,会自动替换掉。
对于保存在堆中的数据,需要JS的垃圾回收器了。V8将堆分为新生代和老生代两个区域,并分别通过副垃圾回收器和主垃圾回收器分别处理。处理的机制都是先标记活动对象与非活动对象,回收非活动对象内存,最后进行内存整理(主垃圾回收器)。
新生代:存放生存时间短,且较小的对象。使用 Scavenge 算法,将新生代空间对半分,快写满一半的时候,进行一次清理,将标记的活动对象复制到另一半,再顺序排列,然后两半翻转。
老生代: 存放生存时间相对长,且较大的对象。使用标记 - 清除(Mark-Sweep)算法,递归遍历调用栈,能够到达的对象标记为活动对象,无法到达的则为垃圾数据。清除之后还需要通过标记 - 整理(Mark-Compact)算法来进行内存整理。
同源策略
同源:域名、端口、协议相同。
同源策略限制:
DOM层面:限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。
可以通过 window.postMessage
来实现非同源页面DOM通信,原理是通过监听消息的方式,本质上还是操作自身DOM。
数据层面:限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。
网络层面:限制 ajax 请求非同源网站数据。
解决跨域的方法
务器端代理,让同源策略对代理服务器生效,再由代理服务器转发请求,绕过跨域限制。
JSONP(JSON with Padding):通过动态创建script标签,利用callback函数的方式来实现跨域请求。
CORS(Cross-Origin Resource Sharing):在服务端设置响应头部信息,允许指定域的访问,从而实现跨域请求。
WebSocket协议:使用WebSocket协议进行跨域通信,WebSocket不受同源策略限制。
跨文档消息传输(Cross-document messaging):在不同域的页面间传递消息,可以通过window.postMessage()方法实现跨域通信
XSS攻击
跨站脚本攻击。指黑客向HTML文件或是DOM中注入恶意脚本来进行攻击。常见攻击手段有窃取 cookie 信息、监听用户行为(键盘操作)、修改DOM伪造表单窃取密码、生成广告等。
根据脚本的注入方式可以分为存储型、反射型和DOM型,前两种都属于服务端攻击,DOM型属于前端攻击。
如何防止
- 对脚本进行过滤或转码。
- 利用内容安全策略( CSP ) ,设置响应头或是mate标签
- 重要cookie使用 HttpOnly 属性,只能在HTTP请求中使用,无法通过脚本获取。
CSRF攻击
CSRF 跨站请求伪造,黑客获取了用户的登录状态,并通过第三方的站点伪造用户请求来进行攻击。
如何防止
- 通过对登录信息等关键 cookie 设置 SameSite 属性。
SameSite 可以设置三个值
- Strict 最严格,完全禁止第三方请求携带Cookie.
- Lax 相对宽松,允许第三方的GET请求或是打开连接可以携带cookie
- None 不限制。
- 在服务端验证请求来源站点,优先验证请求头中的 origin 字段,如果没有,则验证 referer 字段。
- 使用 CSRF Token 。在浏览器发起请求的时候,服务器会生成一个CSRF Token 字符串并返回客户端,后续在转账等敏感请求提交的时候携带上这个token。
浏览器性能优化
DOM树生成
在网络进程收到服务器发来的响应之后,会根据响应头中的 content-type 字段来判断文件类型。如果是 text/html ,此时浏览器就会选择或是创建一个渲染进程,然后建立网络进程与渲染进程的通信管道。
渲染进程内部通过 HTML解析器 来将接收到的HTML字节流转化为DOM结构,边接收边解析。
具体的解析流程:
- 将字节流通过分词器转化为一个个 token 字符串,类似JS的词法分析。
- HTML解析器内部维护了一个栈结构,用来存放 token。遇到起始标签会入栈,终止标签会将对应的起始标签出栈,文本标签不会压入栈中。 在解析起始标签的时候就会创建对应的DOM节点。
JS阻塞DOM执行
如果 HTML 中通过 script
标签内嵌有 JS 脚本,就会阻止DOM的解析,先下载并执行JS脚本,因为脚本中可能有修改DOM节点的操作。
如果确认脚本不会修改DOM,可以使用 async or defer
标记来异步加载
async:异步加载脚本,加载完成之后立即执行,无法保证顺序
defer:延迟加载脚本,在DOMContentLoaded事件之前执行。(DOM解析完毕就执行)
JS执行依赖CSS解析
由于JS内部可能有修改CSS样式的代码,这就要求在JS脚本执行前,CSS文件必须下载完成并解析成JS能够识别并操作的CSSOM结构。
在加载外部JS或是CSS资源并解析的过程,页面可能会解析白屏。缩短白屏时长有如下方法:
- 通过内联JS和CSS来减少或移除这两种文件的下载。
- 如果不能替换,可以通过打包工具,或拆包等手段压缩文件体积。
- 通过媒体查询属性,区分不同场景的CSS
交互阶段
交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
对于一些执行事件过长的脚本,可能会占用