# 1 跨标签页通讯
不同标签页间的通讯,本质原理就是去运用一些可以 共享的中间介质,因此比较常用的有以下方法:
- 通过父页面
window.open()
和子页面postMessage
- 异步下,通过
window.open('about: blank')
和tab.location.href = '*'
- 异步下,通过
- 设置同域下共享的
localStorage
与监听window.onstorage
- 重复写入相同的值无法触发
- 会受到浏览器隐身模式等的限制
- 设置共享
cookie
与不断轮询脏检查(setInterval
) - 借助服务端或者中间层实现
# 2 浏览器架构
- 用户界面
- 主进程
- 内核
- 渲染引擎
JS
引擎- 执行栈
- 事件触发线程
- 消息队列
- 微任务
- 宏任务
- 消息队列
- 网络异步线程
- 定时器线程
# 3 渲染机制
# 3.1 浏览器的渲染机制一般分为以下几个步骤
- 处理
HTML
并构建DOM
树。 - 处理
CSS
构建CSSOM
树。 - 将
DOM
与CSSOM
合并成一个渲染树。 - 根据渲染树来布局,计算每个节点的位置。
- 调用
GPU
绘制,合成图层,显示在屏幕上
- 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢。
- css 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间
- 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM
# 3.2 图层
一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用
- 通过以下几个常用属性可以生成新图层
3D
变换:translate3d
、translateZ
will-change
video
、iframe
标签- 通过动画实现的
opacity
动画转换 position: fixed
# 3.3 重绘与回流
当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中,有两种类型的操作,即重绘与回流。
- 重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此 损耗较少
- 回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。会触发回流的操作:
- 页面初次渲染
- 浏览器窗口大小改变
- 元素尺寸、位置、内容发生改变
- 元素字体大小变化
- 添加或者删除可见的
dom
元素 - 激活
CSS
伪类(例如::hover
) - 查询某些属性或调用某些方法
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
getComputedStyle()
getBoundingClientRect()
scrollTo()
回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。
回流的优化
对树的局部甚至全局重新生成是非常耗性能的,所以要避免频繁触发回流
- 现代浏览器已经帮我们做了优化,采用队列存储多次的回流操作,然后批量执行,但获取布局信息例外,因为要获取到实时的数值,浏览器就必须要清空队列,立即执行回流。
- 编码上,避免连续多次修改,可通过合并修改,一次触发
- 减少
dom
的增删次数,可使用 字符串 或者documentFragment
一次性插入 - 对于大量不同的
dom
修改,可以先将其脱离文档流,比如使用绝对定位,或者display:none
,在文档流外修改完成后再放回文档里中 - 将动画效果应用到
position
属性为absolute
或fixed
的元素上 - 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
- 通过节流和防抖控制触发频率
css3
硬件加速,transform
、opacity
、filters
,开启后,会新建渲染层
另外,可以借助 DevTools Performance
面板来查看产生回流重绘任务占用主线程的情况和调用代码
开启GPU加速的方法
开启后,会将 dom元素提升为独立的渲染层,它的变化不会再影响文档流中的布局。
transform: translateZ(0)
opacity
filters
Will-change
很多人不知道的是,重绘和回流其实和 Event loop
有关
- 当
Event loop
执行完Microtasks
后,会判断document
是否需要更新。因为浏览器是60Hz
的刷新率,每16ms
才会更新一次。 - 然后判断是否有
resize
或者scroll
,有的话会去触发事件,所以resize
和scroll
事件也是至少16ms
才会触发一次,并且自带节流功能。 - 判断是否触发了
media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame
回调 - 执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback
回调
# 3.4 JavaScript 会阻塞 DOM 生成
JavaScript
会阻塞DOM
生成,而样式文件又会阻塞JavaScript
的执行,所以在实际的工程中需要重点关注JavaScript
文件和样式表文件,使用不当会影响到页面性能的
当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线程,如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据
- 如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。
- 而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。
- 不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建 DOM,构建布局树,绘制页面
# 3.5 缩短白屏时长,可以有以下策略
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上
sync
或者defer
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
# 4 跨域
因为浏览器出于安全考虑,有同源策略。也就是说,如果
协议
、域名
、端口
有一个不同就是跨域,Ajax
请求会失败。
我们可以通过以下几种常用方法解决跨域的问题
# 4.1 JSONP
JSONP
的原理很简单,就是利用<script>
标签没有跨域限制的漏洞。通过<script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据
涉及到的端
JSONP
需要服务端和前端配合实现。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script> <script> function jsonp(data) { console.log(data) } </script>
@程序员poetry: 代码已经复制到剪贴板
JSONP
使用简单且兼容性不错,但是只限于get
请求
具体实现方式
- 在开发中可能会遇到多个
JSONP
请求的回调函数名是相同的,这时候就需要自己封装一个JSONP
,以下是简单实现
function jsonp(url, jsonpCallback, success) { let script = document.createElement("script"); script.src = url; script.async = true; script.type = "text/javascript"; window[jsonpCallback] = function(data) { success && success(data); }; document.body.appendChild(script); } jsonp( "http://xxx", "callback", function(value) { console.log(value); } );
@程序员poetry: 代码已经复制到剪贴板
# 4.2 CORS
CORS
(Cross-Origin Resource Sharing,跨域资源共享) 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求
涉及到的端
CORS
只需要服务端/后端支持即可,不涉及前端改动
CORS
需要浏览器和后端同时支持。IE 8
和9
需要通过XDomainRequest
来实现。- 浏览器会自动进行
CORS
通信,实现CORS
通信的关键是后端。只要后端实现了CORS
,就实现了跨域。 - 服务端设置
Access-Control-Allow-Origin
就可以开启CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
CORS
实现起来非常方便,只需要增加一些 HTTP
头,让服务器能声明允许的访问来源
只要后端实现了 CORS
,就实现了跨域
以 koa
框架举例
添加中间件,直接设置Access-Control-Allow-Origin
请求头
app.use(async (ctx, next)=> { ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild'); ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); if (ctx.method == 'OPTIONS') { ctx.body = 200; } else { await next(); } })
@程序员poetry: 代码已经复制到剪贴板
具体实现方式
CORS
将请求分为简单请求(Simple Requests)和需预检请求(Preflighted requests),不同场景有不同的行为
- 简单请求:不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:
- 请求方法:
GET
、HEAD
、POST
。 - 请求头:
Accept
、Accept-Language
、Content-Language
、Content-Type
。Content-Type
仅支持:application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 请求方法:
- 需预检请求:当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个
OPTIONS
请求,通过服务端返回的Access-Control-Allow-*
判定请求是否被允许
CORS
引入了以下几个以 Access-Control-Allow-*
开头:
Access-Control-Allow-Origin
表示允许的来源Access-Control-Allow-Methods
表示允许的请求方法Access-Control-Allow-Headers
表示允许的请求头Access-Control-Allow-Credentials
表示允许携带认证信息
当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求
# 4.3 nginx反向代理
反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可
nginx 配置跨域,可以为全局配置和单个代理配置(两者不能同时配置)
- 全局配置,在
nginx.conf
文件中的http
节点加入跨域信息
http { # 跨域配置 add_header 'Access-Control-Allow-Origin' '$http_origin' ; add_header 'Access-Control-Allow-Credentials' 'true' ; add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ; add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ; }
@程序员poetry: 代码已经复制到剪贴板
- 局部配置(单个代理配置跨域), 在路径匹配符中加入跨域信息
server { listen 8080; server_name server_name; charset utf-8; location / { # 这里配置单个代理跨域,跨域配置 add_header 'Access-Control-Allow-Origin' '$http_origin' ; add_header 'Access-Control-Allow-Credentials' 'true' ; add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ; add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ; #配置代理 代理到本机服务端口 proxy_pass http://127.0.0.1:9000; proxy_redirect off; proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
@程序员poetry: 代码已经复制到剪贴板
# 4.4 Node 中间层接口转发
const router = require('koa-router')() const rp = require('request-promise'); // 通过node中间层转发实现接口跨域 router.post('/github', async (ctx, next) => { let {category = 'trending',lang = 'javascript',limit,offset,period} = ctx.request.body lang = lang || 'javascript' limit = limit || 30 offset = offset || 0 period = period || 'week' let res = await rp({ method: 'POST', // 跨域的接口 uri: `https://e.juejin.cn/resources/github`, body: { category, lang, limit, offset, period }, json: true }) ctx.body = res }) module.exports = router
@程序员poetry: 代码已经复制到剪贴板
# 4.5 Proxy
如果是通过vue-cli
脚手架工具搭建项目,我们可以通过webpack
为我们起一个本地服务器作为请求的代理对象
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域
在vue.config.js
文件,新增以下代码
module.exports = { devServer: { host: '127.0.0.1', port: 8080,
@程序员poetry: 代码已经复制到剪贴板