📍浏览器架构
浏览器架构演进
- 单进程架构:所有模块运行在同一个进程里,包括网络、插件、JavaScript运行环境等。
- 多进程架构(现代浏览器架构):包含主进程、网络进程、渲染进程(运行在沙箱模式下)、GPU进程、插件进程。各个进程负责自己的事情,彼此间相互隔离,提高了安全性,减少了浏览器崩溃的情况。
Chrome的默认策略是,每个标签对应一个渲染进程。但是如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫process-per-site-instance。
- 面向服务架构:也是多进程架构,将UI、数据库、文件、设备、网络等,作为一个独立的基础服务。
浏览器架构对比
老师提供的表格整理的很清晰,直接看表吧
多进程分工
核心还是浏览器主进程,相较于单进程划分粒度更细。
思考
-
为什么有单进程架构?
个人觉得是演进的必然阶段,单进程架构虽然在现在看来不好,但相较于多进程其简单易于实现,而且早期还受硬件的限制。
-
面向服务架构是否会替代多进程架构?
面向服务架构可以看作是多进程架构的升级,不考虑设备因素的话,在未来应该是会的。但是在性能没那么好的设备中多进程架构应该短时间内还是会存在的。
📍渲染进程
常见浏览器内核
💬浏览器内核:是浏览器的核心部分,它是浏览器的渲染引擎,负责将网页的HTML、CSS、JavaScript等代码解析成可视化的网页。
渲染进程-多线程架构
渲染进程内部是多线程实现,主要负责页面渲染、脚本执行、事件处理、网络请求等。
👉补充:进程、线程的概念
进程:是对正在运行中的程序的抽象,是系统进行【资源分配和调度】的基本单位。
线程:是操作系统能够进行【运算调度】的最小单位,是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。
一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存。
每一个渲染进程下都有JS引擎线程,JS引擎线程和渲染线程是互斥的。因此,js执行时间过长会导致页面卡顿。渲染线程负责界面的渲染,会在JS引擎空闲时立即执行。
JS引擎 VS 渲染引擎
- 解析执行JS(一个优化:将常用的函数之类,通过JIT(Just-in-time compilation)编译为机器码)
- XML解析生成渲染树,显示在屏幕
- 桥接方式通信(存在一些延迟)
渲染进程-多线程工作流程⭐
- 网络线程负责加载网页资源(url输入后的第一步,解析地址,建立连接,发送请求)
- JS引擎解析JS脚本并执行
- JS解析空闲时,渲染线程立即工作
- 用户交互、定时器操作等产生回调函数放入任务队列中
- 事件线程进行事件循环,将队列里的任务取出来交给JS引擎执行
❓页面阻塞问题:
总是看到,“尽量将CSS放头部,JS放底部”,一直没去了解原因,借此机会整理一下相关点(可能不够完整)。
- 网络线程负责加载资源,可以并行发送请求。
- 而DOM树解析、布局渲染和JS执行分别隶属于渲染线程和JS引擎,由上文可知,这两个线程是互斥的,只能串行。由此,就会存在页面阻塞的问题。
script标签的defer/async属性,用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。(能够使JS加载不阻塞DOM解析)
- CSS加载不会阻塞DOM树解析,但是会影响页面渲染。(因为:浏览器解析DOM构建DOM树,解析CSS构建CSSOM树,最终根据二者组成render树,再渲染页面。CSS加载会影响到CSSOM树的构建,所以...)
- DOM树解析到非异步的外联 js 时会被阻塞,在它加载并且执行完之前,不会往下解析DOM树,从而也就阻塞了页面渲染。(内联JS的执行也会阻塞DOM 解析)
- 每次碰到
<script>标签时,浏览器都会渲染一次页面,以确保脚本能获取到最新的DOM元素信息。- 一般情况下,CSS加载不会影响DOM解析,但是当头部同时有
<script>与<link>的情况下,最好将<script>放在<link>上面,否则js脚本可能会等待css加载,而导致阻塞。
📍Chrome运行原理
浏览器地址输入URL后发生了什么?
从 浏览器主进程 和 渲染进程 两部分来理解记忆发生的事情。
主进程-输入处理
- 用户URL框输入内容的后,UI线程会判断输入的是一个URL地址,还是一个query查询条件
- 如果是URL,直接请求站点资源
- 如果是query,将输入发送给搜索引擎
主进程-开始导航
- 当用户按下回车,UI线程通知网络线程发起一个网络请求,来获取站点内容
- 请求过程中,tab处于loading状态
主进程-读取响应
- 网络线程接收到HTTP响应后,先检查响应头的媒体类型(MIME Type)
- 如果响应主体是一个HTML文件,浏览器将内容交给渲染进程处理
- 如果拿到的是其他类型文件,比如Zip、exe等,则交给下载管理器处理
主进程-寻找渲染进程
- 网络线程做完所有检查后,会告知主进程数据已准备完毕,主进程确认后为这个站点寻找一个渲染进程
- 主进程通过IPC消息告知渲染进程去处理本次导航
- 渲染进程开始接收数据并告知主进程自己已开始处理,导航结束,进入文档加载阶段
渲染进程-资源加载
- 收到主进程的消息后,开始加载HTML文档
- 加载子资源,如图片、CSS样式文件、JavaScript脚本
渲染进程-构建渲染树
- 构建DOM树,将HTML文本转化成浏览器能够理解的结构
- 构建CSSOM树,浏览器同样不认识CSS,需要将CSS代码转化为可理解的CSSOM
- 构建渲染树,渲染树是DOM树和CSSOM树的结合
渲染进程-页面布局
- 根据渲染树计算每个节点的位置和大小
- 在浏览器页面区域绘制元素边框 3.遍历渲染树,将元素以盒模型的形式写入文档流
渲染进程-页面绘制
- 构建图层:为特定的节点生成专用图层
- 绘制图层:一个图层分成很多绘制指令,然后将这些指令按顺序组成一一个绘制列表,交给合成线程
- 合成线程接收指令生成图块
- 栅格线程将图块进行栅格化(像素点)
- 展示在屏幕上(调用显卡)
📍前端性能 performance
时间花销:(可以借助开发者工具的Performance栏来查看,也可以借助Performance API来编程查看)
- 脚本运行
- 渲染页面
- Painting
首屏优化⭐
- 压缩、分包、删除无用代码
- 静态资源分离(CDN)
- JS脚本非阻塞加载
- 缓存策略(结合HTTP响应头部字段
Cache-Control或Expires使用)- 强制缓存:在第一次请求资源时,将资源缓存在本地,下次请求时直接从本地获取。如果资源没有过期,则直接使用本地缓存,否则重新请求资源。决定是否使用缓存的主动权在浏览器。
- 协商缓存:在第一次请求资源时,将资源的标识符(如
ETag或Last-Modified)保存在本地。下次请求时,将这些标识符发送给服务器,服务器根据标识符判断资源是否有更新。如果没有更新,则返回 304 状态码,浏览器直接使用本地缓存。通过服务端告知客户端是否可以使用缓存。
- SSR(服务器端渲染)
- 预置loading、 骨架屏
渲染优化
- GPU加速(会新建图层,
opacity透明度、transform做动画、使用will-change提前告诉浏览器这个元素需要GPU加速) - 减少回流、重绘(使用transform代替left、top,使用
visibility代替display: none,因为它们会引起布局变化) - 离屏渲染(开辟一块内存,将需要渲染的内容绘制到屏幕外缓冲区中)
- 懒加载(将资源标识为非阻塞资源并仅在需要时加载)
JS优化
- 防止内存泄漏(当已经不需要某块内存时这块内存还存在着),发生原因:
- 全局变量
- 未清除的定时器
- 未清除的DOM元素引用
- 闭包
- 循环引用等
- 循环尽早break
- 合理使用闭包
- 减少DOM访问:JS引擎和渲染线程通信需要使用Bridge,比较耗时
- 防抖、节流
- 防抖:连续快速触发的事件,只会执行最后一次,前面的都被取消。
- 节流:在指定的时间间隔内不会重复执行事件,只有大于这个时间间隔才会再次执行事件,把频繁的触发变为少量触发。
- Web Workers(是 HTML5 中的一项新技术,它允许在浏览器后台运行脚本,从而避免了阻塞用户界面)
📍跨端容器
为什么需要跨端
- 开发成本、效率的考虑:一套代码可以在各端运行
- 一致性体验
- 前端开发生态
跨端方案
-
webview:网页视图
- 一次开发处处使用,学习成本低
- 随时发布,即时更新
一个简易的JSBridge实现:(现在还不太理解,暂且搁置)
-
小程序:双线程
-
React Native/WeeX:原生组件渲染
-
Lynx:DOM构建放在Native层(JSBinding是高效的JSBridge)
-
Flutter
跨端方案对比
果然MindMap会更清晰
总结
同样是干货满满的一堂课,每一个点扩展开都有很多细节。果然很多东西要真的理解还是得从底层原理入手。可能得多回顾才能产生自己的想法吧,现阶段更多的还是一个收集整理的角色,道阻且长哟。