关于浏览器的那些事情1
1 事件机制
document 往事件触发处传播,遇到注册的捕获事件会触发 传播到事件触发处时触发注册的事件
从事件触发处往 document 传播,遇到注册的冒泡事件会触发,事件触发⼀般来说会按照上⾯的顺序进⾏,但是也有特例,如果给⼀个⽬标节 点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执⾏
// 以下会先打印冒泡然后是捕获
node.addEventListener('click',(event) =>{
console.log('冒泡') },false);
node.addEventListener('click',(event) =>{
console.log('捕获 ') },true)
注册事件
通常我们使⽤ addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可 以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false 。
useCapture 决定了注册的事件是捕获事件还是冒泡事件
⼀般来说,我们只希望事件只触发在⽬标上,这时候可以使⽤ stopPropagation 来阻⽌
事件的进⼀步传播。通常我们认为 stopPropagation 是⽤来阻⽌事件冒泡的,其实该函 数也可以阻⽌捕获事件。 stopImmediatePropagation 同样也能实现阻⽌事件,但是还 能阻⽌该事件⽬标执⾏别的注册事件
node.addEventListener('click',(event) =>{
event.stopImmediatePropagation()
console.log('冒泡') },false);
// 点击 node 只会执⾏上⾯的函数,该函数不会执⾏
node.addEventListener('click',(event) => {
console.log('捕获 ') },true)
事件代理
如果⼀个节点中的⼦节点是动态⽣成的,那么⼦节点需要注册事件的话应该注 册在⽗节点上
<ul id="ul"> <li>1</li>
<li>2</li> <li>3</li> <li>4</li> <li>5</li>
</ul> <script>
let ul = document.querySelector('##ul')
ul.addEventListener('click', (event) => {
console.log(event.target); })
</script>
事件代理的⽅式相对于直接给⽬标注册事件来说,有以下优点
节省内存
不需要给⼦节点注销事件
2 跨域
因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端⼝ 有⼀个不同就是跨域, Ajax 请求会失败
JSONP
JSONP 的原理很简单,就是利⽤
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script> <script>
function jsonp(data) {
console.log(data) }
</script>
JSONP 使⽤简单且兼容性不错,但是只限于 get 请求
CORS
CORS 需要浏览器和后端同时⽀持 浏览器会⾃动进⾏ CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了
CORS ,就实现了跨域。 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS 。 该属性表示哪些域名 可以访问资源,如果设置通配符则表示所有⽹站都可以访问资源 document.domain 该⽅式只能⽤于⼆级域名相同的情况下,⽐如 a.test.com 和 b.test.com 适⽤于该⽅式。
只需要给⻚⾯添加 document.domain = ‘test.com’ 表示⼆级域名都相同就可以实现跨域
postMessage
这种⽅式通常⽤于获取嵌⼊⻚⾯中的第三⽅⻚⾯数据。⼀个⻚⾯发送消息,另
⼀个⻚⾯判断来源并接收消息
// 发送消息端
window.parent.postMessage('message', 'http://blog.poetries.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://blog.poetries.com') {
console.log('验证通过')
} });
3 Event loop
JS中的event loop
众所周知 JS 是⻔⾮阻塞单线程语⾔,因为在最初 JS 就是为了和浏览器交 互⽽诞⽣的。
如果 JS 是⻔多线程的语⾔话,我们在多个线程中处理 DOM
就可能会发⽣问题(⼀个线程中新加节点,另⼀个线程中删除节点)
JS 在执⾏的过程中会产⽣执⾏环境,这些执⾏环境会被顺序的加⼊到执⾏栈中。如果遇 到异步的代码,会被挂起并加⼊到 Task (有多种 task ) 队列中。⼀旦执⾏栈为空,
Event Loop 就会从 Task 队列中拿出需要执⾏的代码并放⼊执⾏栈中执⾏,所以本 质上来说 JS 中的异步还是同步⾏为
console.log('script start');
setTimeout(function() {
console.log('setTimeout'); }, 0);
console.log('script end');
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务 ( microtask ) 和 宏任务( macrotask )。在 ES6 规范中,
microtask 称为 jobs,macrotask 称为 task
console.log('script start');
setTimeout(function() {
console.log('setTimeout'); }, 0);
new Promise((resolve) => {
console.log('Promise')
resolve() }).then(function() {
console.log('promise1'); }).then(function() {
console.log('promise2'); });
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTime
以上代码虽然 setTimeout写在Promise 之前,但是因为 Promise 属于微任务⽽ setTimeout 属于宏任务
微任务
process.nextTick
promise
Object.observe
MutationObserver
宏任务
script
setTimeout
setInterval
setImmediate
I/O
UI rendering
宏任务中包括了 script ,浏览器会先执⾏⼀个宏任务,接下来有异步代码 的话就先执⾏微任务
so 。。正确的⼀次 Event loop 顺序是这样的
执⾏同步代码,这属于宏任务
执⾏栈为空,查询是否有微任务需要执⾏
执⾏所有微任务
必要的话渲染 UI 然后开始下⼀轮 Event loop ,执⾏宏任务中的异步代码
通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有⼤量的计算 并且需要操作 DOM 的话,为了更快的响应界⾯响应,我们可以把操作 DOM
放⼊微任务中
Node 中的 Event loop
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为 6 个阶段,它们会按照顺序反复运⾏
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timer
timers 阶段会执⾏ setTimeout 和 setInterval
⼀个 timer 指定的时间并不是准确时间,⽽是在达到这个时间后尽快执⾏回调,可能会因 为系统正在执⾏别的事务⽽延迟
I/O
I/O 阶段会执⾏除了 close 事件,定时器和 setImmediate 的回调
poll
poll 阶段很重要,这⼀阶段中,系统会做两件事情
执⾏到点的定时器
执⾏ poll 队列中的事件
并且当 poll 中没有定时器的情况下,会发现以下两件事情 如果 poll 队列不为空,会遍历回调队列并同步执⾏,直到队列为空或者系统限制 如果 poll 队列为空,会有两件事发⽣ 如果有 setImmediate 需要执⾏, poll 阶段会停⽌并且进⼊到 check 阶段执⾏setImmediate
如果没有 setImmediate 需要执⾏,会等待回调被加⼊到队列中并⽴即执⾏回调 如果有别的定时器需要被执⾏,会回到 timer 阶段执⾏回调。
check
check 阶段执⾏ setImmediate
close callbacks
close callbacks 阶段执⾏ close 事件
并且在 Node 中,有些情况下的定时器执⾏顺序是随机的
setTimeout(() => {
console.log('setTimeout'); }, 0);
setImmediate(() => {
console.log('setImmediate'); }
// 这⾥可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进⼊ event loop ⽤了不到 1 毫秒,这时候会执⾏ setImmediate
// 否则会执⾏ setTimeout
上⾯介绍的都是 macrotask 的执⾏情况, microtask 会在以上每个阶段完 成后⽴即执⾏
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
}) }, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
}) }, 0)
// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中⼀定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
Node 中的 process.nextTick 会先于其他 microtask 执⾏
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1"); }); }, 0);
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1
4 Service Worker
Service workers 本质上充当Web应⽤程序与浏览器之间的代理服务器,
也 可以在⽹络可⽤时作为浏览器和⽹络间的代理。
它们旨在(除其他之外)使得 能够创建有效的离线体验,
拦截⽹络请求并基于⽹络是否可⽤以及更新的资源 是否驻留在服务器上来采取适当的动作
。他们还允许访问推送通知和后台同步 API
⽬前该技术通常⽤来做缓存⽂件,提⾼⾸屏速度
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("sw.js")
.then(function(registration) {
console.log("service worker 注册成功");
})
.catch(function(err) {
console.log("servcie worker 注册失败");
}); }
// sw.js
// 监听 `install` 事件,回调中缓存所需⽂件
self.addEventListener("install", e => {
e.waitUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
); });
// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接⽤缓存,否则去请求数据
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response;
}
console.log("fetch source");
})
); });
打开⻚⾯,可以在开发者⼯具中的 Application 看到 Service Worker 已 经启动了
在 Cache 中也可以发现我们所需的⽂件已被缓存
当我们重新刷新⻚⾯可以发现我们缓存的数据是从 Service Worker 中读 取的
5 渲染机制
浏览器的渲染机制⼀般分为以下⼏个步骤
处理 HTML 并构建 DOM 树。
处理 CSS 构建 CSSOM 树。
将 DOM 与 CSSOM 合并成⼀个渲染树。
根据渲染树来布局,计算每个节点的位置。
调⽤ GPU 绘制,合成图层,显示在屏幕上
在构建 CSSOM 树时,会阻塞渲染,直⾄ CSSOM 树构建完成。并且构建 CSSOM 树是⼀ 个⼗分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选 择器,执⾏速度越慢 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地⽅重新开始。 也就是说,如果你想⾸屏渲染的越快,就越不应该在⾸屏就加载 JS ⽂件。并且 CSS 也会
影响 JS 的执⾏,只有当解析完样式表才会执⾏ JS,所以也可以认为这种情况下,CSS 也 会暂停构建 DOM
⼀般来说,可以把普通⽂档流看成⼀个图层。
特定的属性可以⽣成⼀个新的图层。
不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议
单独⽣成⼀ 个新图层,提⾼性能。
但也不能⽣成过多的图层,会引起反作⽤
通过以下⼏个常⽤属性可以⽣成新图层
3D 变换: translate3d 、 translateZ
will-change
video 、 iframe 标签
通过动画实现的 opacity 动画转换
position: fixed
重绘(Repaint)和回流(Reflow)
重绘是当节点需要更改外观⽽不会影响布局的,⽐如改变 color 就叫称为重绘 回流是布局或者⼏何属性需要改变就称为回流
回流必定会发⽣重绘,重绘不⼀定会引发回流。
回流所需的成本⽐重绘⾼的多,
改变深层次的节点很可能导致⽗节点的⼀系列回流
所以以下⼏个动作可能会导致性能问题:
改变 window ⼤⼩
改变字体 添加或删除样式
⽂字改变
定位或者浮动
盒模型
很多⼈不知道的是,重绘和回流其实和 Event loop 有关
当 Event loop 执⾏完 Microtasks 后,会判断 document 是否需要更新。
因为浏览 器是 60Hz 的刷新率,每 16ms 才会更新⼀次。
然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和
scroll 事件也是⾄少 16ms 才会触发⼀次,并且⾃带节流功能。
判断是否触发了 media query
更新动画并且发送事件
判断是否有全屏操作事件
执⾏ requestAnimationFrame 回调
执⾏ IntersectionObserver 回调,该⽅法⽤于判断元素是否可⻅,
可以⽤于懒加载上,但是兼容性不好。
更新界⾯
以上就是⼀帧中可能会做的事情。如果在⼀帧中有空闲时间,就会去执⾏
requestIdleCallback 回调
减少重绘和回流
使⽤ translate 替代 top
使⽤ visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流
(改变了布局)
不要使⽤ table 布局,可能很⼩的⼀个⼩改动会造成整个 table 的重新布局
动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使⽤
requestAnimationFrame
CSS 选择符从右往左匹配查找,避免 DOM 深度过深
将频繁运⾏的动画变为图层,图层能够阻⽌该节点回流影响别的元素。
⽐如对于 video
标签,浏览器会⾃动将该节点变为图层
三、性能
1 DNS 预解析
DNS 解析也是需要时间的,可以通过预解析的⽅式来预先获得域名所对应的 IP
<link rel="dns-prefetch" href="//blog.poetries.top">
2 缓存
缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提 ⾼⽹⻚的整体加载速度 通常浏览器缓存策略分为两种:强缓存和协商缓存
强缓存
实现强缓存可以通过两种响应头实现: Expires 和 Cache-Control 。强缓 存表示在缓存期间不需要请求, state code 为 200
Expires: Wed, 22 Oct 2018 08:41:00 GMT
Expires 是 HTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018
08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如 果修改了本地时间,可能会造成缓存失效
Cache-control: max-age=30
Cache-Control 出现于 HTTP / 1.1 ,优先级⾼于 Expires 。
该属性表示资源会在 30 秒后过期,需要再次请求
协商缓存
如果缓存过期了,我们就可以使⽤协商缓存来解决问题。协商缓存需要请求,
如果缓存有 效会返回 304
协商缓存需要客户端和服务端共同实现,和强缓存⼀样,也有两种实现⽅式
Last-Modified 和 If-Modified-Since
Last-Modified 表示本地⽂件最后修改⽇期, If-Modified-Since 会将 LastModified 的值发送给服务器,
询问服务器在该⽇期后资源是否有更新,有更新的话就会将 新的资源发送回来
但是如果在本地打开缓存⽂件,就会造成 Last-Modified 被修改,
所以在 HTTP /
1.1 出现了 ETag
ETag 和 If-None-Match
ETag 类似于⽂件指纹, If-None-Match 会将当前 ETag 发送给服务器,
询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。
并且 ETag 优先级⽐ LastModified ⾼
选择合适的缓存策略
对于⼤部分的场景都可以使⽤强缓存配合协商缓存解决,
但是在⼀些特殊的地⽅可能需要选择特殊的缓存策略
对于某些不需要缓存的资源,可以使⽤ Cache-control: no-store ,
表示该资源不需要缓存
对于频繁变动的资源,可以使⽤ Cache-Control: no-cache 并配合 ETag 使⽤,
表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。
对于代码⽂件来说,通常使⽤ Cache-Control: max-age=31536000 并配合策略缓存使 ⽤,然后对⽂件进⾏指纹处理,⼀旦⽂件名变动就会⽴刻下载新的⽂件
3 使⽤ HTTP / 2.0
因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建⽴和断开,
消耗 了好⼏个 RTT 时间,并且由于 TCP 慢启动的原因,
加载体积⼤的⽂件会需要更多的时 间在 HTTP / 2.0 中引⼊了多路复⽤,
能够让多个请求使⽤同⼀个 TCP 链接,极⼤的加 快了⽹⻚的加载速度。
并且还⽀持 Header 压缩,进⼀步的减少了请求的数据⼤⼩
4 预加载
在开发中,可能会遇到这样的情况。有些资源不需要⻢上⽤到,
但是希望尽早获取,这时 候就可以使⽤预加载 预加载其实是声明式的 fetch ,
强制浏览器请求资源,并且不会阻塞 onload 事件,
可以使⽤以下代码开启预加载、
<link rel="preload" href="http://example.com">
预加载可以⼀定程度上降低⾸屏的加载时间,
因为可以将⼀些不影响⾸屏但重要的⽂件延后加载,唯⼀缺点就是兼容性不好
5 预渲染
可以通过预渲染将下载的⽂件预先在后台渲染,可以使⽤以下代码开启预渲染
<link rel="prerender" href="http://poetries.com">
预渲染虽然可以提⾼⻚⾯的加载速度,但是要确保该⻚⾯百分百会被⽤户在之后打开,否 则就⽩⽩浪费资源去渲染
6 懒执⾏与懒加载
懒执⾏
懒执⾏就是将某些逻辑延迟到使⽤时再计算。该技术可以⽤于⾸屏优化,对于某些耗时逻 辑并不需要在⾸屏就使⽤的,就可以使⽤懒执⾏。懒执⾏需要唤醒,⼀般可以通过定时器 或者事件的调⽤来唤醒
懒加载
懒加载就是将不关键的资源延后加载
懒加载的原理就是只加载⾃定义区域(通常是可视区域,
但也可以是即将进⼊ 可视区域)内需要加载的东⻄。
对于图⽚来说,先设置图⽚标签的 src 属性 为⼀张占位图,
将真实的图⽚资源放⼊⼀个⾃定义属性中,当进⼊⾃定义区域 时,
就将⾃定义属性替换为 src 属性,这样图⽚就会去下载资源,实现了图⽚懒加载
懒加载不仅可以⽤于图⽚,也可以使⽤在别的资源上。⽐如进⼊可视区域才开始播放视频 等
7 ⽂件优化
图⽚优化
对于如何优化图⽚,有 2 个思路
减少像素点
减少每个像素点能够显示的颜⾊
图⽚加载优化
不⽤图⽚。很多时候会使⽤到很多修饰类图⽚,
其实这类修饰图⽚完全可以⽤ CSS 去代 替。对于移动端来说,屏幕宽度就那么点,
完全没有必要去加载原图浪费带宽。⼀般图⽚都⽤ CDN 加载,
可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图⽚⼩图使⽤ base64 格式
将多个图标⽂件整合到⼀张图⽚中(雪碧图) 选择正确的图⽚格式:
对于能够显示 WebP 格式的浏览器尽量使⽤ WebP 格式。
因为 WebP 格式具有更好 的图像数据压缩算法,能带来更⼩的图⽚体积,
⽽且拥有⾁眼识别⽆差异的图像质量, 缺点就是兼容性并不好⼩图使⽤ PNG ,
其实对于⼤部分图标这类图⽚,完全可以使⽤ SVG 代替
照⽚使⽤ JPEG
其他⽂件优化
CSS ⽂件放在 head 中 服务端开启⽂件压缩功能将 script 标签放在 body 底部,
因为 JS ⽂件执⾏会阻塞渲染。当然也可以把script 标签放在任意位置
然后加上 defer ,表示该⽂件会并⾏下载,但是会放到HTML 解析完成后顺序执⾏。
对于没有任何依赖的 JS ⽂件可以加上 async ,表示加 载和渲染后续⽂档元素的过程
将和 JS ⽂件的加载与执⾏并⾏⽆序进⾏。 执⾏ JS 代码 过⻓会卡住渲染,
对于需要很多时间计算的代码 可以考虑使⽤ Webworker 。
Webworker 可以让我们另开⼀个线程执⾏脚本⽽不影响渲染。
CDN
静态资源尽量使⽤ CDN 加载,由于浏览器对于单个域名有并发请求上限,
可 以考虑使⽤多个 CDN 域名。对于 CDN 加载静态资源需要注意 CDN 域名
要与主站不同,否则每次请求都会带上主站的 Cookie
8 其他
使⽤ Webpack 优化项⽬
对于 Webpack4 ,打包项⽬使⽤ production 模式,这样会⾃动开启代码压缩
使⽤ ES6 模块来开启 tree shaking ,这个技术可以移除没有使⽤的代码优化图⽚,
对于⼩图可以使⽤ base64 的⽅式写⼊⽂件中
按照路由拆分代码,实现按需加载 给打包出来的⽂件名添加哈希,
实现浏览器缓存⽂件
监控
对于代码运⾏错误,通常的办法是使⽤ window.onerror 拦截报错。该⽅法 能拦截到⼤部分的详细报错信息,但是也有例外
对于跨域的代码运⾏错误会显示 Script error .
对于这种情况我们需要给 script 标签添加 crossorigin 属性
对于某些浏览器可能不会显示调⽤栈信息,这种情况可以
通过arguments.callee.caller 来做栈递归
对于异步代码来说,可以使⽤ catch 的⽅式捕获错误。
⽐如 Promise 可以直接使⽤ catch 函数, async await 可以使⽤ try catch
但是要注意线上运⾏的代码都是压缩过的,
需要在打包时⽣成 sourceMap ⽂件便于
debug 。 对于捕获的错误需要上传给服务器,
通常可以通过 img 标签的 src 发起⼀个请求
每日一句中文式外语
法语
你好,我叫XXX
笨猪,热辣木屋XX(一头笨猪,在热辣的木屋里,那头笨猪就是XXX)
【哈哈!好吧,我笑了,这个联想记忆太骚了哈哈哈】