课程目标
本节课程主要着重介绍浏览器的架构以及运行原理,并以一道八股文为例,讲解在Chrome浏览器里,网页是如何加载并且渲染成我们所见的画面。通过对本节课的学习,我们了解到在浏览器里JS引擎和渲染引擎如何协同工作,如何从多个角度优化前端的性能体验。再以webview容器为扩展,认识一些常见的跨端方案。
浏览器架构
浏览器架构演进
- 单进程架构:所有模块运行在同一个进程里,包含网络、插件、JavaScript 运行环境等。
- 多进程架构:主进程、网络进程、渲染进程、GPU 进程、插件进程。
- 面向服务架构:将原来的 UI、数据库、文件、设备、网络等,作为一个独立的基础服务。
浏览器架构对比
| 架构类型 | 扩展性 | 安全性 | 稳定性 | 流畅度 |
|---|---|---|---|---|
| 单进程架构 | 低,所有模块运行在同一进程里,访问同一块内存区域,数据没有隔离,新增模块可能会影响原有功能 | 低,三方插件可直接访问操作系统里任意资源 | 低,三方插件漏洞或者某个 tab 页面 JavaScript 脚本问题可能导致浏览器崩溃 | 卡顿,所有页面运行在同一进程中,开启多个页面时明显卡顿 |
| 多进程架构 | 中,各进程分配独立的内存区域,有些进程功能较大,耦合度高 | 高,运行在独立沙箱中,不能访问系统敏感资源 | 高,进程相互隔离,当一个页面或者插件崩溃时,不会影响其他进程 | 流畅,每个页面运行在独立的渲染进程中,充分利用系统资源 |
| 面向服务架构 | 高, 服务模块划分更细,更内聚,耦合性低,易于扩展 | 高,运行在独立沙箱中,不能访问系统敏感资源 | 高,进程相互隔离,当一个页面或者插件崩溃时,不会影响其他进程 | 流畅,每个页面运行在独立的渲染进程中,充分利用系统资源 |
从任务管理器查看资源占用情况
多进程分工
| 进程名称 | 进程描述 |
|---|---|
| 浏览器(主进程) | 主要负责页面展示逻辑,用户交互,子进程管理;包括地址栏、书签、前进、后退、收藏夹等 |
| GPU 进程 | 负责 UI 绘制,包含整个浏览器全部 UI |
| 网络进程 | 网络服务进程,负责网络资源加载 |
| ==标签页(渲染进程)== | 控制 tab 内的所有内容,将 Html、Css 和 JavaScript 转换为用户可交互的网页 |
| 插件进程 | 控制网站运行的插件,比如 flash、ModHeader 等 |
| 其他进程 | 如上图所示:适用程序 Storage/Network/Audio Service 等 |
思考:
- 为什么会有单进程架构
- 面向服务架构是否会提点多进程架构
渲染进程
常见浏览器内核
多线程架构
内部是多线程实现,主要负责页面渲染,脚本执行,事件处理,网络请求等
| 线程 | 功能 |
|---|---|
| JS 引擎 | 负责解析 js 脚本、运行 js 程序,每个渲染进程下面只有一个 js 引擎线程。与 GUI 渲染进程互斥,如果 js 任务执行事件过长,会导致页面卡顿 |
| GUI 渲染 | 负责渲染浏览器界面,解析 html、css,构建 dom 树和 render 树、布局、绘制、和 js 引擎线程互斥,GUI 更新会在 js 引擎空闲时立即执行 |
| 定时器触发 | 定时器所在线程,setTimeout、setInterval 计时完毕后,将回调添加到事件队列,等待 js 引擎执行 |
| 网络线程 | 在 XHR、Fetch 等发起请求后新开一个网络线程请求,如果设置了回调函数,在状态变更时,将回调放入事件队列,等待 js 引擎执行 |
| 事件触发 | 由宿主环境提供,用于控制事件循环,不断的从事件队列例取出任务执行 |
JS 引擎 vs 渲染引擎
- 解析执行 JS
- XML 解析生成渲染树,显示在屏幕
- 桥接方式通信
注:字节码->机器码 是一种优化,因为机器码可被操作系统直接执行,所以这种优化提高了效率。
多线程工作流程
- 网络线程负责加载网页资源
- JS 引擎解析 JS 脚本并且执行
- JS 解析引擎空闲时,渲染线程立即工作
- 用户交互、定时器操作等产生回调函数放入任务队列中
- 事件线程进行事件循环,将队列例的任务取出交给 JS 引擎执行
JS 执行顺序
event loop
Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制。JavaScript 语言就采用这种机制,来解决单线程运行带来的一些问题。
接下来我们要了解 event loop 的执行顺序:
- 首先执行 script 宏任务。
- 执行 同步任务,遇见微任务进入微任务队列,遇见宏任务进入宏任务队列。
- 当前 宏任务执行完出队,检查 微任务 列表,有则依次执行,直到全部执行完。
- 执行浏览器 UI 线程的 渲染 工作。
- 检查是否有 Web Worker 任务,有则执行。
- 执行 下一个宏任务,回到第二步,依此循环,直到宏任务和微任务队列都为空。
微任务包括:MutationObserver、Promise.then() 或 catch()、Promise为基础开发的其它技术,比如 fetch API、V8的垃圾回收过程、Node独有的 process.nextTick 、 Object.observe(已废弃;Proxy 对象替代)。
宏任务包括: script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering 、 postMessage 、 MessageChannel。
下面是一些例题(都是在浏览器环境下执行)
同步 + Promise
例一:
var promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(()=>{
console.log(3)
})
console.log(4)
//输出结果:
// 1
// 2
// 4
// 3
解析:
- 首先明确,Promise 构造函数是同步执行的,then 方法是异步执行的
- 开始
new Promise,执行构造函数同步代码,输出 1 - 再
resolve(),将promise的状态改为了resolved,并且将resolve值保存下来,此处没有传值 - 执行构造函数同步代码,输出 2
- 跳出
promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列 - 执行同步代码,输出 4
- 此时宏任务执行完毕,开始检查微任务队列,执行
promise.then微任务,输出 3
例二:
var promise = new Promise((resolve, reject) => {
console.log(1)
})
promise.then(()=>{
console.log(2)
})
console.log(3)
//输出结果:
// 1
// 3
解析:
- 开始
new Promise,执行构造函数同步代码,输出 1 - 再
promise.then,因为promise中并没有resolve,所以then方法不会执行 - 执行同步代码,输出 3
例三:
var promise = new Promise((resolve, reject) => {
console.log(1)
})
promise.then(console.log(2))
console.log(3)
//输出结果:
// 1
// 2
// 3
解析:
- 首先明确,
.then或者.catch的参数期望是函数,传入非函数则会发生值透传 (value => value) - 开始
new Promise,执行构造函数同步代码,输出 1 - 然后
then()的参数是一个console.log(2)(注意:并不是一个函数),是立即执行的,输出 2 - 执行同步代码,输出 3
例四:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
//输出结果:
// 1
解析:
then(2)、then(Promise.resolve(3))发生了值穿透,直接执行最后一个then,输出 1
例五:
var promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
reject()
})
promise.then(()=>{
console.log(2)
}).catch(()=>{
console.log(3)
})
console.log(4)
//输出结果:
// 1
// 4
// 2
解析:
- 开始
new Promise,执行构造函数同步代码,输出 1 - 再
resolve(),将promise的状态改为了resolved,并且将resolve值保存下来,此处没有传值 - 再
reject(),此时promise的状态已经改为了resolved,不能再重新翻转(状态转变只能是 pending -> resolved 或者 pending -> rejected,状态转变不可逆) - 跳出
promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列 - 往下执行,碰到
promise.catch这个微任务,此时promise的状态为 resolved(非rejected),忽略catch方法 - 执行同步代码,输出 4
- 此时宏任务执行完毕,开始检查微任务队列,执行
promise.then微任务,输出 2
例六:
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
//输出结果:
// 1
// 2
解析:
- 首先
resolve(1),状态改为了resolved,并且将resolve值保存下来 - 执行
console.log(res)输出 1 - 返回
return 2,实际上是包装成了resolve(2) - 状态为
resolved,catch方法被忽略 - 最后
then,输出 2
同步 + Promise + setTimeout
例一:
setTimeout(() => {
console.log(1)
})
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
//输出结果:
// 3
// 2
// 1
解析:
在解析宏任务与微任务时,我们先使用一个公式来理解执行顺序:
同步宏任务 > 微任务 > 微任务队列 > 宏任务队列。
- 首先
setTimeout被放入宏任务队列 - 再
Promise.resolve().then,then方法被放入微任务队列 - 执行同步代码,输出 3
- 此时宏任务执行完毕,开始检查微任务队列,执行
then微任务,输出 2 - 微任务队列执行完毕,检查执行一个宏任务
- 发现
setTimeout宏任务,执行 输出 1
例二:
var promise = new Promise((resolve, reject) => {
console.log(1)
setTimeout(() => {
console.log(2)
resolve()
}, 1000)
})
promise.then(() => {
console.log(3)
})
promise.then(() => {
console.log(4)
})
console.log(5)
//输出结果:
// 1
// 5
// 2
// 3
// 4
解析:
- 首先明确,当遇到
promise.then时,如果当前的Promise还处于pending状态,我们并不能确定调用resolved还是rejected,只有等待promise的状态确定后,再做处理,所以我们需要把两种情况的处理逻辑做成callback放入promise的回调函数组内,当promise状态翻转为resolved时,才将之前的promise.then推入微任务队列 - 开始,
Promise构造函数同步执行,输出 1,执行setTimeout - 将
setTimeout加入到宏任务队列中 - 然后,第一个
promise.then放入promise的回调数组内 - 第二个
promise.then放入promise的回调函数内 - 执行同步代码,输出 5
- 检查微任务队列,为空
- 检查宏任务队列,执行
setTimeout宏任务,输入 2 ,执行resolve,promise状态翻转为resolved,将之前的promise.then推入微任务队列 setTimeout宏任务出队,检查微任务队列- 执行第一个微任务,输出 3
- 执行第二个微任务,输出 4
最后
var date = new Date()
console.log(1, new Date() - date)
setTimeout(() => {
console.log(2, new Date() - date)
}, 500)
Promise.resolve().then(console.log(3, new Date() - date))
while(new Date() - date < 1000) {}
console.log(4, new Date() - date)
解析:
- 首先执行同步代码,输出 1 0
- 遇到
setTimeout,定时500ms后执行,此时,将setTimeout交给异步线程,主线程继续执行下一步,异步线程执行setTimeout - 主线程执行
Promise.resolve().then,.then的参数不是函数,发生(value => value) ,输出 3 1 - 主线程继续执行同步任务
whlie,等待1000ms,在此期间,setTimeout定时500ms完成,异步线程将setTimeout回调事件放入宏任务队列中 - 继续执行下一步,输出 4 1000
- 检查微任务队列,为空
- 检查宏任务队列,执行
setTimeout宏任务,输入 2 1000
总结
Promise构造函数是 同步执行 的,then方法是 异步执行 的.then或者.catch的参数期望是函数,传入非函数则会发生值透传Promise的状态一经改变就不能再改变,构造函数中的resolve或reject只有第一次执行有效,多次调用没有任何作用.then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch是.then第二个参数的简便写法- 当遇到
promise.then时, 如果当前的Promise还处于pending状态,我们并不能确定调用resolved还是rejected,只有等待promise的状态确定后,再做处理,所以我们需要把我们的两种情况的处理逻辑做成callback放入promise的回调数组内,当promise状态翻转为resolved时,才将之前的promise.then推入微任务队列
Chrome 运行原理
如何展示网页
浏览器地址输入 URL 后发生了什么?
- 输入解析:浏览器首先解析输入的内容,判断它是一个有效的URL还是搜索查询。如果输入的内容被识别为搜索查询,浏览器将使用默认的搜索引擎进行搜索。
- DNS 查询:如果输入的内容是一个有效的 URL,浏览器将进行域名系统(DNS)查询以将域名解析为对应的 IP 地址。浏览器会首先检查本地 DNS 缓存,如果找不到对应的记录,它会向 DNS 服务器发送查询请求。
- 建立连接:浏览器与目标服务器建立一个 TCP 连接。这通常包括三次握手过程,以确保双方都准备好进行数据传输。
- 发送 HTTP 请求:TCP 连接建立后,浏览器向服务器发送一个 HTTP 请求。请求通常包括请求方法(如 GET 或 POST)、请求的资源路径、HTTP 协议版本、请求头(包含诸如用户代理、接受的编码和语言等信息)以及可能的请求体(如 POST 请求所包含的表单数据)。
- 接收响应:服务器处理 HTTP 请求,并将响应数据发送回浏览器。响应通常包括 HTTP 状态码(如 200 表示成功,404 表示未找到等)、响应头(包含诸如内容类型、内容长度等信息)以及响应体(通常是 HTML 文档)。
- 关闭或重用连接:一旦浏览器接收到完整的响应数据,它可以选择关闭 TCP 连接或将其保持在活动状态以用于后续请求。
- 解析 HTML:浏览器解析 HTML 文档,构建 DOM 树。在解析过程中,浏览器可能遇到
<script>标签或其他需要立即执行的脚本,这时浏览器将暂停解析,执行脚本,然后继续解析。 - 请求其他资源:HTML 文档通常包含其他资源的引用,如 CSS、JavaScript 和图片等。浏览器将发送额外的 HTTP 请求来获取这些资源。这些请求可能与初始请求的服务器相同,也可能涉及其他服务器。
- 构建渲染树:浏览器解析 CSS 样式,并将其应用于 DOM 树,生成渲染树。渲染树包含了所有可见元素及其样式信息。
- 布局:浏览器计算渲染树中每个元素的位置和大小,生成布局信息。
- 绘制:浏览器根据渲染树和布局信息将元素绘制到屏幕上。这个过程称为绘制或渲染。浏览器会将各个层的元素绘制到位图中,然后将这些位图合成到屏幕上显示的最终图像。
- 交互:在完成页面绘制后,浏览器开始接收和处理用户的交互,如点击、滚动、输入等。这些交互可能会触发 JavaScript 事件处理程序,从而修改 DOM 或应用新的样式。这些修改可能会导致浏览器重新布局和绘制页面的部分或全部内容。
- 关闭或卸载:当用户导航到其他页面或关闭浏览器选项卡时,浏览器将触发相应的页面卸载事件,如
beforeunload和unload。这给开发者一个机会来执行清理操作,如保存用户数据或取消挂起的网络请求。一旦完成这些操作,浏览器将卸载页面并释放相关资源。
输入处理
输入处理:浏览器主进程会根据用户输入的URL进行解析和处理,包括检查URL的有效性、判断是否需要使用搜索引擎、判断是否需要使用HTTPS等等。在处理完输入之后,浏览器主进程将发起开始导航的请求。
- 用户 URL 框输入内容后,UI 线程会 判断输入 的是一个 URL 地址还是一个 query 查询条件。
- 如果是 URL,则直接请求站点资源。
- 如果是 query,将输入发送给搜索引擎
开始导航
开始导航:浏览器主进程会向网络进程发送开始导航的请求,以便进行后续的页面加载操作。在发送请求之前,浏览器主进程会先判断当前页面是否支持复用,如果支持,则会重用渲染进程,否则会创建一个新的渲染进程来加载页面。
- 当用户按下回车,UI 线程通知网络线程 发起一个网络请求,来获取站点内容。
- 请求过程中,tab 处于 loading 状态。
读取响应
读取响应:一旦网络进程开始加载页面,它会读取服务器响应的数据,并将其返回给浏览器主进程。在读取响应的过程中,网络进程会进行数据解码和安全检查等操作。
- 网络线程接收到 HTTP 响应后,先 检查 响应头的 媒体类型 (MIME Type)。
- 如果响应主体是一个 HTML 文件,浏览器将内容交给渲染进程处理。
- 如果拿到的是其他类型文件,比如 zip、exe 等,则交给下载管理器处理。
寻找渲染进程
寻找渲染进程:一旦浏览器主进程收到网络进程返回的响应数据,它会根据响应数据中的渲染进程ID来寻找对应的渲染进程。如果已经存在对应的渲染进程,则将响应数据发送给该进程进行处理;否则,浏览器主进程会创建一个新的渲染进程来处理该页面。
- 网络线程做完所有检查后,会告知主进程数据已准备完毕,主进程确认后为这个站点寻找一个渲染进程。
- 主进程通过 IPC 消息告知 渲染进程区 处理本次导航。
- 渲染进程开始接收数据并告知主进程自己已经开始处理,导航结束,进入文档加载阶段。
渲染进程
资源加载
- 收到主进程的消息后,开始加载 HTML 文档。
- 除此之外,还需要加载子资源,比如一些图片,CSS 样式文件以及 JavaScript 脚本。
构建渲染树
- 构建 DOM 树,将 HTML 文本转化成浏览器能够理解的结构。
- 构建 CSSOM 树,浏览器同样不认识 CSS,需要将 CSS 代码转化为可理解的 CSSOM。
- 构建渲染树,渲染树是 DOM 树和 CSSOM 树的结合。
页面布局
- 根据渲染树计算每个节点的位置和大小。
- 在浏览器页面区域绘制元素边框。
- 遍历渲染树,将元素以盒模型的形式写入文档流。
页面绘制
- 构建图层:为特定的节点生成专用图层。
- 绘制图层:一个图层分成很多绘制指令,然后将这些指令按顺序组成一个绘制列表,交给 合成线程。
- 合成线程接收指令生成 图块。
- 栅格线程将图块进行栅格化。
- 展示在屏幕上。
前端性能 performance
Performance是一个做前端性能监控离不开的API,最好在页面完全加载完成之后再使用,因为很多值必须在页面完全加载之后才能得到。最简单的办法是在window.onload事件中读取各种数据。
performance对象中常用的属性和方法:
| 方法 | 结果 |
|---|---|
navigationStart | 浏览器开始导航到当前页面的时间戳。 |
fetchStart | 浏览器开始检索文档的时间戳。 |
responseStart | 浏览器收到第一个字节的时间戳。 |
domLoading | 浏览器开始解析文档的时间戳。 |
domInteractive | 浏览器完成解析文档的时间戳。 |
domContentLoadedEventStart | 浏览器完成解析文档的时间戳。 |
domComplete | 文档解析完成的时间戳。 |
| loadEventStart | 文档加载事件开始的时间戳。 |
loadEventEnd | 文档加载事件结束的时间戳。 |
最常用的是 navigationStart 和 loadEventEnd。navigationStart 表示浏览器开始导航到当前页面的时间戳,loadEventEnd 表示文档加载事件结束的时间戳。这两个时间戳之差就是页面的加载时间。
时间都花在哪?
performance.timing
这个 API 能帮我们得到整个页面请求的时间,如下图,在 Chrome 的 Console 是可以直接运行的
以下解释这些数据代表什么:
fetchStart:发起获取当前文档的时间点,我的理解是浏览器收到发起页面请求的时间点;domainLookupStart:返回浏览器开始DNS查询的时间,如果此请求没有DNS查询过程,如长连接、资源cache、甚至是本地资源等,那么就返回fetchStart的值;domainLookupEnd:返回浏览器结束DNS查询的时间,如果没有DNS查询过程,同上;connectStart:浏览器向服务器请求文档,开始建立连接的时间,如果此连接是一个长连接,或者无需与服务器连接(命中缓存),则返回domainLookupEnd的值;connectEnd:浏览器向服务器请求文档,建立连接成功的时间;requestStart:开始请求文档的时间(注意没有requestEnd);responseStart:浏览器开始接收第一个字节数据的时间,数据可能来自于服务器、缓存、或本地资源;unloadEventStart:卸载上一个文档开始的时间;unloadEventEnd:卸载上一个文档结束的时间;domLoading:浏览器把document.readyState设置为“loading”的时间点,开始构建dom树的时间点;responseEnd:浏览器接收最后一个字节数据的时间,或连接被关闭的时间;domInteractive:浏览器把document.readyState设置为'interactive'的时间点,DOM树创建结束;domContentLoadedEventStart:文档发生DOMContentLoaded事件的时间;domContentLoadedEventEnd:文档的DOMContentLoaded事件结束的时间;domComplete:浏览器把document.readyState设置为"complete"的时间点;loadEventStart:文档触发load事件的时间;loadEventEnd:文档出发load事件结束后的时间;
从以上的分析,我们就可以得到一些时间的计算:
- 准备新页面耗时:
fetchStart - navigationStart - 重定向时间:
redirectEnd - redirectStart App Cache时间:domainLookupStart - fetchStartDNS解析时间:domainLookupEnd -domainLookupStartTCP连接时间:connectEnd - connectStartrequest时间:responseEnd - requestStart这个计算是代表请求响应加起来的时间- 请求完毕到
DOM树加载:domInteractive -responseEnd - 构建与解析
DOM树,加载资源时间:domCompleter -domInteractive load时间:loadEventEnd - loadEventStart- 整个页面加载时间:
loadEventEnd -navigationStart - 白屏时间:
responseStart-navigationStart
performance.getEntries()
这个API能帮我们获得资源的请求时间,包括JS、CSS、图片等。
如上图可以看到这个 API 请求返回的是一个数组,这个数组包括整个页面所有的资源加载,上图打开了一个其中一个资源,可以看到如下信息:
entryType:类型为resourcename:资源的urlinitiatorType:资源是link- 资源时间:
duration的值,是responseEnd - startTime得到的
什么情况下卡顿?
卡顿一般是由于浏览器在渲染页面时遇到了阻塞或耗时操作导致的。以下是一些可能导致卡顿的情况:
- JavaScript 执行时间过长:JavaScript 的执行会阻塞页面的渲染。如果脚本执行时间过长,会导致页面无响应或卡顿现象。
- 大量的 DOM 操作:DOM 操作会影响页面的渲染,特别是在大量DOM操作时。如果需要频繁地更新DOM元素,可以考虑使用虚拟 DOM 等技术来减少 DOM 操作次数。
- 大量的网络请求:浏览器在渲染页面时需要下载和解析HTML、CSS、JavaScript和图像等资源。如果有大量的资源需要下载,会导致页面加载时间过长。
- 大量的样式和布局计算:如果页面包含大量的样式和布局计算,会影响页面的渲染性能。
- 阻塞渲染的 JavaScript:如果 脚本阻塞了页面的渲染,就会导致卡顿或页面无响应。
首屏优化
-
压缩、分包、删除无用代码
通过压缩代码、分包加载和删除无用代码等技术,可以减小页面的体积,加快页面的加载速度。
-
静态资源分离
将页面中的静态资源(如CSS、JavaScript和图像等)与 HTML 文档分离,可以使得浏览器可以并行加载这些资源,从而提高页面的加载速度。
-
JS 脚本非阻塞加载
将 JS 脚本异步加载,可以减少页面的渲染阻塞,从而提高页面的加载速度。可以使用
defer和async等属性来实现 JS 的非阻塞加载。 -
缓存策略
合理地设置缓存策略,可以减少对服务器的请求,加快页面的加载速度。可以使用 HTTP 响应头中的
Cache-Control和Expires等属性来设置缓存策略。 -
SSR
服务器端渲染(Server Side Rendering)可以在服务器端生成 HTML 文档,减少客户端渲染的工作量,从而提高页面的加载速度。SSR适用于复杂的单页面应用或对 SEO 有要求的应用。
-
预置loading、骨架屏
在页面加载过程中,可以预置一个 loading 动画或骨架屏,以提高用户体验。这些技术可以在页面加载完成之前,先显示一些占位元素,给用户一个等待的感觉,从而减少用户等待的焦虑和不安。
渲染优化
-
GPU 加速
将复杂的图形处理任务交给GPU来处理,可以加快页面的渲染速度。可以使用 CSS3 的
transform和opacity等属性来开启GPU加速。 -
减少回流、重绘
回流和重绘是影响页面性能的主要因素之一。可以通过避免使用影响布局的属性、批量修改 DOM 元素等技术来减少回流和重绘操作。
-
离屏渲染
离屏渲染是将页面中的部分内容在单独的图层中进行渲染,从而减少对主渲染线程的阻塞。可以使用 CSS3 的
transform和position等属性来开启离屏渲染。 -
懒加载
将页面中的非必要资源(如图片和视频等)延迟加载,可以加快页面的加载速度。可以使用
Intersection Observer和Lazyload等技术来实现懒加载。
JS 优化
-
防止内存泄漏
-
有可能出现内存泄漏的场景
- 全局变量:全局变量会一直存在于内存中,直到程序结束才会被释放。如果程序中定义了大量的全局变量,就会导致内存占用过多,从而导致内存泄漏。
- 闭包:闭包会在函数中保存局部变量和参数,如果函数执行后,闭包中的变量没有被释放,就会导致内存泄漏。为了避免内存泄漏,应该合理使用闭包,并注意释放不需要的变量。
- 循环引用:循环引用是指两个或多个对象之间相互引用,形成了一个死循环,导致内存无法释放。为了避免循环引用,应该及时释放不需要的引用,并使用垃圾回收机制来自动释放内存。
- 定时器和事件监听器:定时器和事件监听器会持续占用内存,直到被清除或被解除绑定。如果程序中存在大量的定时器和事件监听器,就会导致内存占用过多,从而导致内存泄漏。
- DOM 节点:DOM 节点也会占用内存空间,如果程序中存在大量的 DOM 节点,就会导致内存占用过多,从而导致内存泄漏。为了避免内存泄漏,应该及时清除不需要的 DOM 节点
-
内存泄漏会导致不必要的内存占用和程序崩溃。可以使用
let和const关键字声明变量,避免变量污染和内存泄漏
-
-
循环尽早 break
在循环中,如果已经找到了需要的结果,可以使用
break语句尽早结束循环,避免无用的迭代和计算。 -
合理使用闭包
闭包可以在函数中保存局部变量和参数,避免全局变量的污染和泄漏。但是,如果使用不当,也会导致内存泄漏和性能下降。
-
减少 Dom 访问
DOM 操作是 JavaScript 性能的一个瓶颈。可以使用缓存和批量操作等技术来减少 DOM 访问次数,从而提高 JavaScript 的性能。
-
防抖、节流
防抖(debounce): 防抖是指在一定时间内,如果连续触发事件,那么只执行一次目标函数。常用于输入框实时搜索、窗口大小调整等场景。
防抖的实现原理:设置一个定时器,在指定的延迟时间内,如果再次触发事件,则重新计时。只有在延迟时间内没有再次触发事件时,才会执行目标函数。
节流(throttle): 节流是指在一定时间内,无论触发多少次事件,目标函数都只执行一次。常用于滚动事件、鼠标移动等场景。
节流的实现原理是:设置一个间隔时间,在这个时间内,无论事件触发多少次,都只执行一次目标函数。一旦超过这个间隔时间,就会再次执行目标函数。
总结: 防抖和节流的 主要区别 在于,防抖 是在一定时间内只执行一次目标函数,而 节流 是在一定时间内控制目标函数执行次数。它们都可以有效地减少函数执行频率,降低性能开销,提高用户体验。
防抖和节流是用来控制函数调用频率的技术。可以使用
setTimeout和requestAnimationFrame等 API 来实现防抖和节流(或者用第三方库也行)。 -
Web Workers
Web Workers 是一种在后台线程中执行 JavaScript 代码的技术。可以将耗时的计算任务和数据处理等操作放到 Web Workers 中执行,避免阻塞主线程,提高页面的响应速度。
跨端容器
为什么需要跨端
-
开发成本、效率
跨端开发可以帮助降低成本和提高开发效率。使用跨端技术,开发者只需编写一份代码,就可以在多个平台(如iOS、Android和Web)上运行。这可以减少开发和维护的工作量,节省时间和资源。同时,开发团队可以更快地推出新功能和修复问题,因为他们只需关注一份代码库。
-
一致性体验
跨端开发可以确保在不同平台上提供一致的用户体验。使用跨端技术,开发者可以更容易地保持应用的外观和功能一致,无论用户在什么设备上使用。这有助于提高用户满意度和用户留存率。
-
前端开发生态
跨端开发受益于强大的前端生态系统。许多流行的前端框架和库,如 React Native、Flutter和Ionic,都支持跨端开发。这些工具为开发者提供了丰富的资源和丰富的社区支持,帮助他们更轻松地实现跨端功能。
有哪些跨端方案
WebView
简介
WebView,即网页视图,用于加载网页 URL,并展示其内容的控件。可以嵌在移动端 App 内,实现前端混合开发,大多数混合框架都是基于 Webview 的二次开发:比如 lonic、Cordova
优点:
- 跨平台兼容性:使用 WebView,开发者可以利用 Web 技术为多个平台(如iOS和Android)创建应用。这可以节省开发时间和成本,一次开发处处使用,学习成本低,同时确保应用在各个平台上提供一致的用户体验。
- 代码重用:WebView 允许开发者在移动应用中重用现有的 Web 代码。这意味着,对于已有 Web 应用的公司来说,可以更容易地将其产品扩展到移动平台。
- 简化更新过程:随时发布,即时更新,不用下载安装包。WebView使得应用内容的更新变得更加简单。因为 WebView 直接加载 Web 内容,开发者可以在服务器端更新应用内容,而无需重新提交整个应用到应用商店进行审核。
- 通过 JSBridge 和原生系统交互,能够实现一些更加复杂的功能。
局限性:
性能:相比于原生应用,基于 WebView 的应用性能可能较差。这主要是因为 WebView 需要加载和运行 Web 内容,这会消耗更多的系统资源。在某些情况下,这可能导致较慢的加载速度和不流畅的用户体验。
原生功能访问限制:虽然 WebView 提供了一些与原生功能交互的能力,但它仍然受到一定的限制。为了实现某些特定的原生功能,开发者可能需要编写额外的平台特定代码。
用户体验差异:尽管 WebView 可以确保跨平台的一致性,但它可能无法完全符合不同平台的设计规范。因此,在某些情况下,基于 WebView 的应用可能无法提供与原生应用相同水平的用户体验。
WebView 分离
常用 webview:Android、IOS、国产 Android。
- WebView(Android):这是 Android 平台的原生 WebView 组件,用于在 Android 应用中加载并显示 Web 内容。根据 Android 版本和设备制造商的不同,WebView 的表现可能会有所差异。这可能导致一些兼容性和性能问题。
- X5 WebView(腾讯X5内核):这是由腾讯公司推出的一种 WebView 解决方案,用于解决 Android 系统上 WebView 的碎片化问题。X5 内核基于腾讯 QQ 浏览器的内核,提供了更稳定、更高性能的 WebView 组件。它可以在各种 Android 设备和系统版本上提供一致的表现,减少兼容性问题。
- UIWebView(iOS,已弃用):这是 iOS 平台的原生 WebView 组件,用于在iOS应用中加载并显示 Web 内容。 UIWebView 自 iOS 2.0 开始引入,但在 iOS 8.0 中被 WKWebView 取代。自 iOS 12.0 以来,UIWebView 已被官方弃用,不再推荐使用。
- WKWebView(iOS):这是 iOS 平台上的新一代 WebView 组件,取代了已弃用的 UIWebView。WKWebView 自 iOS 8.0 开始引入,它具有更好的性能和更丰富的功能,如支持多进程、JavaScript 性能改进等。苹果公司推荐开发者使用 WKWebView 来在 iOS 应用中加载并显示 Web 内容。
使用原生能力
JavaScript 调用 Native
- API 注入:Native 获取 Javascipt 环境上下文,对其挂载的对象或者方法进行拦截
- 使用 Webview URL Scheme 跳转拦截
- IOS 上 window.webkit.messageHandler 直接通信
Native 调用 JavaScript
- 直接通过 webview 暴露的 API 执行 JS 代码
- IOS webview.stringByEvaluatingJavaScriptFromString
- Android webview.evaluateJavascript
WebView 与 Native 通信
- JS 环境中提供通信的 JSBridge
- Native 端提供 SDK 响应的 JSBridge 发出的调用
- 前端和客户端分别实现对应功能模块
实现一个简易 JSBridge
interface CallAge {
callId: string // 调用Id,唯一标识
module: string // 调用模块
method: string // 调用方法
data: any // 参数
}
const Callbacks = {} // 存放回调函数 callId 为 key
function applyNative = (payload:CallArgs, callback:Function)=> {
const callId = prefix + callTime++
Callbacks[callId] = callback
const Args0: CallArgs = {
callId: callId,
module: payload.module || 'layout',
method: payload.method || 'randomSize',
data: payload.data,
}
if (IOS) {
return window.webkitURL.messageHandler.postMessage(JSON.stringify(Args0))
} else {
// Android 对 window 上约定的对象进行拦截
return window.AndroidBridge(JSON.stringify(Args0))
}
}
interface RemponseArgs {
responseId: string // 回调Id,与 callId 对应
errCode: number
errMsg?: string
data: unknow
}
// native 端调用 webview,参数都经过序列化
const applyWebview = (res:string)=> {
const response = JSON.parse(res) as ResponseArgs
const {responseId} = response
// 从 Callbacks 找到对应的回调处理方法
if (type of Callbacks[responseId] === 'function') {
Callbacks[responseId](response)
// 回调后删除该次回调函数
delete Callbacks[responseId]
}
}
window.JSBridge = {
applyNative,
applyWebview // 挂载在 window 上,供 native 直接调用
}
小程序
- 微信、支付宝、百度小程序、小米直达号
- 渲染层 - webview
- 双线程,多 webview 架构
- 数据通信,Native 转发
React Naive / WeeX
React Native
React Native 是 Facebook 开发的一个开源框架,它允许开发者使用 React 和 JavaScript 编写原生移动应用。React Native 的主要特点是 "Learn once, write anywhere"(学习一次,随处编写),这意味着开发者可以在不同平台(如 iOS 和 Android)上使用相同的技术栈构建原生应用。
优势:
- 代码复用:React Native 允许开发者在多个平台上复用大部分代码,从而减少开发时间和成本。
- 热重载:React Native 支持热重载功能,允许开发者在不重新编译整个应用的情况下查看代码更改的效果,提高开发效率。
- 原生性能:虽然 React Native 使用 JavaScript 编写,但它将 JavaScript 代码转换为原生组件,从而提供了接近原生应用的性能。
- 丰富的生态系统:React Native 拥有庞大的社区支持,开发者可以利用许多第三方库和组件来加速开发过程。
WeeX
Weex 是由阿里巴巴开发的一个开源框架,它允许开发者使用 Vue.js 和 JavaScript 编写原生移动应用。Weex 的目标是实现 "Write once, run everywhere"(编写一次,随处运行),即使用一套代码构建多个平台的原生应用。
优势:
- 代码复用:类似于 React Native,Weex 也支持在不同平台上复用代码,从而降低开发成本。
- 原生渲染:Weex 将 Vue.js 组件转换为原生组件进行渲染,从而实现了较高的性能。
- 插件系统:Weex 提供了一个丰富的插件系统,开发者可以使用这些插件来轻松实现原生功能,如地图、支付等。
- 模块化:Weex 允许开发者将应用拆分为多个模块,这有助于实现高度模块化的开发过程,提高代码可维护性。
渲染流程
- 原生组件渲染
- React / Vue 框架
- virtual dom
- JSBridge
Lynx
-
基于 Vue 框架
Lynx 采用 Vue.js 作为其基础框架,允许开发者使用 Vue.js 编写应用。Vue.js 是一种渐进式 JavaScript 框架,它使开发者能够轻松地构建可扩展、高性能的应用。基于 Vue.js 的设计原则,Lynx 可以提供简洁、模块化的代码结构,以及良好的开发体验。
-
绑定于 JS Core / V8
Lynx 选择 JS Core(iOS平台)或 V8(Android平台)作为其 JavaScript 引擎。这意味着 Lynx 应用在运行时,JavaScript 代码将在高性能的 JavaScript 引擎中执行。通过使用这些优秀的 JavaScript 引擎,Lynx 能够确保应用在不同平台上具有稳定、高性能的运行表现。
-
JSBinding
Lynx 使用 JSBinding 技术实现 JavaScript 与原生代码之间的通信。这种技术允许 JavaScript 直接调用原生方法,并使原生代码能够执行 JavaScript 回调。通过 JSBinding,Lynx 实现了高效的原生与 JavaScript 之间的通信,降低了性能损失。
-
Native UI / Skia
Lynx 使用 Native UI 组件和 Skia 作为其渲染引擎。Native UI 组件意味着 Lynx 应用在运行时,界面将使用原生组件进行渲染。这可以确保应用具有接近原生应用的性能和用户体验。同时,Lynx 还采用了 Skia 图形库,它是一种高性能的 2D 图形渲染引擎,用于绘制图形和文本。Skia 使 Lynx 应用在渲染复杂界面时能够保持流畅的帧率和高质量的视觉效果。
Flutter
Flutter 是 Google 开发的一个 开源 UI 工具包,旨在为开发者提供一种构建优美、高性能的跨平台应用的解决方案。Flutter 具有一些独特的特点,包括基于 Widget 的设计、Dart VM 以及使用 Skia 图形库。
-
wideget
在 Flutter 中,所有UI元素都被称为Widget。Widget 是 Flutter 应用的基本构建块,它们可以嵌套、组合以及自定义,从而创建复杂的用户界面。Flutter 提供了丰富的预制 Widget,如文本、按钮、列表等,开发者可以直接使用这些 Widget,也可以通过组合和扩展它们来构建自定义的 Widget。
-
dart vm
Flutter 使用 Dart 语言进行开发,Dart 是一种强类型、面向对象的编程语言,它既可以编译成 JavaScript 代码(用于Web应用),也可以编译成机器码(用于移动应用)。
在移动端,Flutter 应用运行在 Dart VM(虚拟机)中。Dart VM 提供了即时编译(JIT)和预编译(AOT)两种编译方式。在开发过程中,Dart VM 采用即时编译,这使得 Flutter 具有热重载功能,开发者可以在不重新编译整个应用的情况下查看代码更改的效果。在发布应用时,Dart VM 会采用预编译,将 Dart 代码编译成高效的机器码,以提高应用的性能
-
skia 图形库
Flutter 使用 Skia 图形库进行 UI 渲染。Skia 是一种高性能的 2D 图形渲染引擎,用于绘制图形和文本。由于 Flutter 直接使用 Skia 进行渲染,它无需依赖于原生 UI 组件,可以实现统一的跨平台 UI 渲染。这使得 Flutter 应用具有高度的可定制性,同时还保持了流畅的性能和优美的视觉效果。
通用原理
- UI 组件
- 渲染引擎
- 逻辑控制引擎
- 通信桥梁
- 底层 API 抹平表现差异
跨端方案对比
思考:
- 同样是基于 webview 渲染,为什么小程序体验比 webview 流畅
- 未来的跨端方案是什么