- 主流浏览器: IE/Edge,Chrome,Safari,Opera,Firefox
- 浏览器内核
a. chrome chromium
b. Safari webkit
c. ie trident
d. firfox gecko
e. opera presto - 组成:
a. HTML 解释器 :将 HTML 词法解析输出 DOM 树
b. css 解释器 : 将 css 词法解析输出样式规则
c. 图层布局计算模块 : 布局计算没的对象的位置及大小
d. 视图绘制模块 : 将具体节点渲染形成图像
e. js 引擎 : 编译执行 js 代码 - 浏览器渲染过程
解析 HTML 文档的同时,解析 css 内容,当两个部分都同时解析完成,会生成一个渲染树(Render Tree),然后图层布局计算模块会从根节点开始进行遍历,
计算每个元素的大小和位置,得到渲染树的布局渲染树(Layout of the Render Tree),最后视图会将布局渲染树渲染到计算机上 - 优化层次
- css 层面优化
css 解析器在读取我们的代码时,执行顺序是从右往左- 避免使用通配符
- 关注可以通过继承实现的属性,避免重复匹配重复定义
- 少用标签选择器,如果可以使用类选择器提代
- 减少嵌套,后代的开销是极大的,因此我们可以把深度降低(最高不超过 3 层)
- css 和 js 加载顺序
- css 阻塞: CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。 将 CSS 放在 head 标签里,启用 CDN 实现静态资源加载速度的优化
- js 阻塞: JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。js 存在三种加载方式:
- 正常模式 这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。
<script src="index.js"></script>
- async 模式 JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。
<script async src="index.js"></script>
- defer 模式 JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
<script defer src="index.js"></script>
- 减少 DOM 操作
- 回流: 当我们对 DOM 的修改引发了几何尺寸的变化(宽,高,隐藏)时,浏览器会重新计算元素的位置和大小,这样其他元素的位置和大小也会受到相应的影响,等所有的计算完成,浏览器才会重新绘制,这个过程称为回流(重拍)
- 重绘: 当我们对 DOM 的样式修改未引发其几何变化,浏览器只会改变其样式
- 重绘不一定导致回流,回流一定引发重绘
- js 优化操作 DOM:由于 js 引擎和渲染引擎时独立的两个部分,因此如果 js 操作渲染的逻辑,这样两边通讯会带来消耗,举个例子:
在这里,每次调用 DOM 接口会带来一定的消耗,这时我们可以缓存一些不必要的消耗;而且我们是每次遍历取拼接,这样会修改长度和高度,导致了回流for(let i = 0 ;i < 10000; i++> ) { document.getElementById('container').innerHTML+='<span>我是一个小测试</span>' }
let containerDom = document.getElementById('container') let count = 10000; let content = '' for(let i = 0 ;i < count; i++> ) { content+='<span>我是一个小测试</span>' } containerDom.innerHTML = content
- DocumentFragment :表示一个没有父级文件的最小文档对象, 用于存储已排好版的或尚未打理好格式的 XML 片段;不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题
let container = document.getElementById('container') // 创建一个DOM Fragment对象作为容器 let content = document.createDocumentFragment() for(let count=0;count<10000;count++){ // span此时可以通过DOM API去创建 let oSpan = document.createElement("span") oSpan.innerHTML = '我是一个小测试' // 像操作真实DOM一样操作DOM Fragment对象 content.appendChild(oSpan) } // 内容处理好了,最后再触发真实DOM的更改 container.appendChild(content)
- Event Loop
- 概念: 微任务(micro-task)和宏任务(macro-task)
- 微任务(micro-task): Promise, process.nextTick, MutationObserver
- 宏任务(macro-task): setTimeOut, setInterval, I/O 操作,script 等
- 过程:
- 初始状态:调用栈空,micro 队列为空,macro 队列存在一个 script 标签任务(整体 js 逻辑)
- 全局上下文被推入调用栈,同步代码执行,在此过程会出现一些 micro 或 macro,他们会被推进各自的任务队列中,当同步代码执行完成,script 脚本会被踢出 macro 队列
- 执行 micro 队列中的任务
总结:因此需要异步操作 DOM 的操作,应该包装成 micro
- 异步更新
- Vue 的 $nextTick()
let callbacks = [] let pending = false export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁 if (!pending) { // 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上) pending = true // 是否要求一定要派发为macro任务 if (useMacroTask) { macroTimerFunc() } else { // 如果不说明一定要macro 你们就全都是micro microTimerFunc() } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
- Lighthouse 查看我们网页的一些体验得分,包含:页面性能、PWA、可访问性、最佳实践、SEO
- Perfomance API
- css 层面优化