一、浏览器是如何渲染UI的?
- 浏览器获取HTML文件,然后对文件进行自上而下解析,形成DOM Tree,触发DomContendLoaded事件。
- 与此同时,进行CSS解析,生成Style Rules
- 接着将DOM Tree与Style Rules合成为 Render Tree
- 接着进入布局(Layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标
- 随后调用GPU进行绘制(Paint),遍历Render Tree的节点,并将元素呈现出来
相关过程思考:
1、CSS是否会阻塞DOM的解析? 不会
由上文可见,DOM树解析跟CSSOM解析,是互不影响的,两个并行过程,因此,CSS不会阻塞DOM的解析。
2、CSS是否会阻塞DOM的渲染? 会
根据以上的流程,可以知道,当cssom还没构建完成时,页面是不会渲染到浏览器界面的(渲染时需等css加载完毕,因为render树需要css信息),因此CSS会阻塞DOM的渲染。
其实这里也是浏览器的一种优化机制。因为你加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞DOM树渲染的话,那么当css加载完之后,DOM树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以我干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,在根据最终的样式来渲染DOM树,这种做法性能方面确实会比较好一点。
3、CSS是否会阻塞后续JS语句的执行? 会
会影响到js脚本的执行。因为js脚本不仅可以读取修改到dom,也可以读取修改到cssom。故在js脚本执行前,browser必须保证到css文件完全加载并解析完成,即cssom树完全构建好。这就导致了js执行的延迟,也因此导致html解析和渲染延迟。
(这就是css阻塞js执行,阻塞渲染的根本原因)
4、一些解决方法
1、在引入顺序上,css资源的引入要优于js脚本的引入
2、对css进行精简并尽快提供
3、可以用媒体类型(会加载不会阻塞)
4、用媒体查询(会记载,只有在符合的设备上才会进行阻塞)
<link href="style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">
第一个声明阻塞渲染,适用于所有情况。
第二个声明同样阻塞渲染:“all”是默认类型,如果不指定任何类型,则隐式设置为“all”。因此,第一个和第二个声明实一样的。
第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
最后一个声明只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。
最后,请注意“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论哪一种情况,浏览器仍会下载 CSS 资源,只不过不阻塞渲染的资源优先级较低罢了。然而这些等待的时间是完全不必要的。
注意:
与 同时在头部的话, 在上可能会更好。因为link在上的话,浏览器会先加载css,而后面body里的内容会被前面的js阻塞,原本body中的图片是可以和css并行加载的,现在只能等css执行完,js执行完,在执行(不太确定)。不过只是可能,是因为如果 的内容下载更快的话,是没影响的,但反过来的话, JS 就要等待了
5、js加载是否影响DOM的解析和渲染、js执行、css加载?(全部都影响)
通俗的原理:浏览器并不知道脚本的内容是什么,如果先行解析下面的DOM,万一脚本内全删了后面的DOM,浏览器就白干活了。更别谈丧心病狂的document.write。浏览器无法预估里面的内容,那就干脆全部停住,等脚本执行完再干活就好了。
JS的阻塞
1、所有浏览器在下载JS的时候,会阻止其他的一切活动(比如其他资源的下载,内容的呈现等等)。为了提高用户体验,新一代浏览器都支持并行下载JS,但是JS下载还是会阻止其他资源的加载(图片、css文件等等)
2、原因:浏览器为了防止出现JS修改DOM树,需要重构DOM树的情况,就会阻止其他资源的下载和呈现
3、嵌入JS会阻止所有内容的呈现,但是外部JSD只会阻止其后内容的显示
(因为嵌入js是不需要加载直接可以执行的)
4、浏览器遇到 <script>
标签时,会触发页面渲染,这也就解释了上文中为什么js要等到css加载完。因为浏览器不知道脚本里面是什么,所以在脚本之前会先渲染一次页面,(cssom和dom tree合成render tree),确保脚本能获取到最新的dom信息,尽管可能js并不需要。
6、对于js阻塞,有两种解决方法
defer(延迟执行)和async(异步执行)。这两种方法只适合与引入式的js脚本,不适合inline-script。
defer是立即下载但延迟执行,加载后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。
async是立即下载并执行,加载和渲染后续文档元素的过程将和js脚本的加载与执行并行进行(异步)。
与defer的区别是async会在加载完成后就执行,但是不会影响阻塞到解析和渲染。但是还是会阻塞load事件,所以async-script会可能在DOMcontentloaded触发前或后执行,但是一定会在load事件前触发。
加载顺序:defer按加载顺序执行,async是不按加载顺序执行,加载完就立即执行
二、浏览器如何解析css选择器?
浏览器会『从右往左』解析CSS选择器。
我们知道DOM Tree与Style Rules合成为 Render Tree,实际上是需要将Style Rules 附着到DOM Tree上,因此需要根据选择器提供的信息对DOM Tree进行遍历,才能将样式附着到对应的DOM元素上。
以下这段css为例
.mod-nav h3 span {font-size: 16px;}
我们对应的DOM Tree 如下
若从左向右的匹配,过程是:
- 从 .mod-nav 开始,遍历子节点 header 和子节点 div
- 然后各自向子节点遍历。在右侧 div 的分支中
- 最后遍历到叶子节点 a ,发现不符合规则,需要回溯到 ul 节点,再遍历下一个 li-a,一颗DOM树的节点动不动上千,这种效率很低。
如果从右至左的匹配:
- 先找到所有的最右节点 span,对于每一个 span,向上寻找节点 h3
- 由 h3再向上寻找 class=mod-nav 的节点
- 最后找到根元素 html 则结束这个分支的遍历。
后者匹配性能更好,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点);而从左向右的匹配规则的性能都浪费在了失败的查找上面。
三、DOM Tree是如何构建的?
- 转码: 浏览器将接收到的二进制数据按照指定编码格式转化为HTML字符串
- 生成Tokens: 之后开始parser,浏览器会将HTML字符串解析成Tokens
- 构建Nodes: 对Node添加特定的属性,通过指针确定 Node 的父、子、兄弟关系和所属 treeScope
- 生成DOM Tree: 通过node包含的指针确定的关系构建出DOM
Tree
四、浏览器重绘与重排的区别?
- 重排: 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素
- 重绘: 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变
单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分
重排和重绘代价是高昂的,它们会破坏用户体验,并且让UI展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。
『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。
五、如何触发重排和重绘?
任何改变用来构建渲染树的信息都会导致一次重排或重绘:
- 添加、删除、更新DOM节点
- 通过display: none隐藏一个DOM节点-触发重排和重绘
- 通过visibility: hidden隐藏一个DOM节点-只触发重绘,因为没有几何变化
- 移动或者给页面中的DOM节点添加动画
- 添加一个样式表,调整样式属性
- 用户行为,例如调整窗口大小,改变字号,或者滚动。
何时会引起重绘和回流
触发回流的属性:
1、盒子模型相关属性修改
2、定位属性及浮动
3、改变节点内部文字结构
- width
- height
- padding
- margin
- display
- border-width
- border
- min-height
- top
- bottom
- left
- right
- position
- float
- clear
- text-align
- overflow-y
- font-weight
- overflow
- line-height
- vertical-align
- white-space
- font-size
只发生重绘的属性:
- color
- border-style
- border-radius
- visibility
- text-decoration
- background
- background-image
- background-position
- background-repeat
- background-size
- outlien-color
- outline
- outline-style
- outline-width
- box-shadow
渲染的过程:
1、获取dom后分割为多个图层
2、对每个图层的节点计算样式结果
3、对每个节点生成图形和位置(layout--回流和重布局)
4、将每个节点绘制填充到图层位图中(重绘)
5、图层作为纹理上传至GPU
6、符合多个图层到页面上生成最终屏幕图像(图层重组)
六、如何避免重绘或者重排?
1、集中改变样式
我们往往通过改变class的方式来集中改变样式
// 判断是否是黑色系样式
const theme = isDark ? 'dark' : 'light'
// 根据判断来设置不同的class
ele.setAttribute('className', theme)
2、使用DocumentFragment
我们可以通过createDocumentFragment创建一个游离于DOM树之外的节点,然后在此节点上批量操作,最后插入DOM树中,因此只触发一次重排
var fragment = document.createDocumentFragment();
for (let i = 0;i<10;i++){
let node = document.createElement("p");
node.innerHTML = i;
fragment.appendChild(node);
}
document.body.appendChild(fragment);
3、提升为合成层
将元素提升为合成层有以下优点:
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
提升合成层的最好方式是使用 CSS 的 will-change 属性:
#target {
will-change: transform;
}
关于合成层的详解请移步无线性能优化:Composite
4、使用translate代替top
top会触发layout,translate不会
5、不要使用table布局
table布局可能很小的改动会造成整个table的重新布局
6、开启GPU硬件加速
七、前端如何实现即时通讯?
1、短轮询
短轮询的原理很简单,每隔一段时间客户端就发出一个请求,去获取服务器最新的数据,一定程度上模拟实现了即时通讯。
- 优点:兼容性强,实现非常简单
- 缺点:延迟性高,非常消耗请求资源,影响性能
2、comet
comet有两种主要实现手段,一种是基于 AJAX 的长轮询(long-polling)方式,另一种是基于 Iframe 及 htmlfile 的流(streaming)方式,通常被叫做长连接。
具体两种手段的操作方法请移步Comet技术详解:基于HTTP长连接的Web端实时通信技术
长轮询优缺点:
- 优点:兼容性好,资源浪费较小
- 缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护
长连接优缺点:
- 优点:兼容性好,消息即时到达,不发无用请求
- 缺点:服务器维护长连接消耗资源
3、SSE
使用指南请看Server-Sent Events 教程
SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术。
- 优点:基于HTTP而生,因此不需要太多改造就能使用,使用方便,而websocket非常复杂,必须借助成熟的库或框架
- 缺点:基于文本传输效率没有websocket高,不是严格的双向通信,客户端向服务端发送请求无法复用之前的连接,需要重新发出独立的请求
4、Websocket
使用指南请看WebSocket 教程
Websocket是一个全新的、独立的协议,基于TCP协议,与http协议兼容、却不会融入http协议,仅仅作为html5的一部分,其作用就是在服务器和客户端之间建立实时的双向通信。
- 优点:真正意义上的实时双向通信,性能好,低延迟
- 缺点:独立与http的协议,因此需要额外的项目改造,使用复杂度高,必须引入成熟的库,无法兼容低版本浏览器
5、Web Worker
后面性能优化部分会用到,先做了解
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行
6、Service workers
后面性能优化部分会用到,先做了解
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理,创建有效的离线体验。
八、什么是浏览器同源策略?
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
下表给出了相对http://store.company.com/dir/page.html同源检测的示例:
浏览器中的大部分内容都是受同源策略限制的,但是以下三个标签可以不受限制:
<img src=XXX>
<link href=XXX>
<script src=XXX>
九、如何实现跨域?
跨域是个比较古老的命题了,历史上跨域的实现手段有很多,我们现在主要介绍三种比较主流的跨域方案,其余的方案我们就不深入讨论了,因为使用场景很少,也没必要记这么多奇技淫巧。
1、最经典的跨域方案jsonp
jsonp本质上是一个Hack,它利用<script>
标签不受同源策略限制的特性进行跨域操作。
jsonp优点:
- 实现简单
- 兼容性非常好
jsonp的缺点:
- 只支持get请求(因为
<script>
标签只能get) - 有安全性问题,容易遭受xss攻击
- 需要服务端配合jsonp进行一定程度的改造
jsonp的实现:
function JSONP({
url,
params,
callbackKey,
callback
}) {
// 在参数里制定 callback 的名字
params = params || {}
params[callbackKey] = 'jsonpCallback'
// 预留 callback
window.jsonpCallback = callback
// 拼接参数字符串
const paramKeys = Object.keys(params)
const paramString = paramKeys
.map(key => `${key}=${params[key]}`)
.join('&')
// 插入 DOM 元素
const script = document.createElement('script')
script.setAttribute('src', `${url}?${paramString}`)
document.body.appendChild(script)
}
JSONP({
url: 'http://s.weibo.com/ajax/jsonp/suggestion',
params: {
key: 'test',
},
callbackKey: '_cb',
callback(result) {
console.log(result.data)
}
})
2、最流行的跨域方案cors
cors是目前主流的跨域解决方案,跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
如果你用express,可以这样在后端设置
//CORS middleware
var allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://example.com');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
}
//...
app.configure(function() {
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({ secret: 'cool beans' }));
app.use(express.methodOverride());
app.use(allowCrossDomain);
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
在生产环境中建议用成熟的开源中间件解决问题。
3、最方便的跨域方案Nginx
nginx是一款极其强大的web服务器,其优点就是轻量级、启动快、高并发。
现在的新项目中nginx几乎是首选,我们用node或者java开发的服务通常都需要经过nginx的反向代理。
反向代理的原理很简单,即所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再讲请求转发给node或者java服务,这样就规避了同源策略。
#进程, 可更具cpu数量调整
worker_processes 1;
events {
#连接数
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#连接超时时间,服务器会在这个时间过后关闭连接。
keepalive_timeout 10;
# gizp压缩
gzip on;
# 直接请求nginx也是会报跨域错误的这里设置允许跨域
# 如果代理地址已经允许跨域则不需要这些, 否则报错(虽然这样nginx跨域就没意义了)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
# srever模块配置是http模块中的一个子模块,用来定义一个虚拟访问主机
server {
listen 80;
server_name localhost;
# 根路径指到index.html
location / {
root html;
index index.html index.htm;
}
# localhost/api 的请求会被转发到192.168.0.103:8080
location /api {
rewrite ^/b/(.*)$ /$1 break; # 去除本地接口/api前缀, 否则会出现404
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.0.103:8080; # 转发地址
}
# 重定向错误页面到/50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
4、其它跨域方案
- HTML5 XMLHttpRequest 有一个API,postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
- WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了,因此可以跨域。
- window.name + iframe:window.name属性值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值,我们可以利用这个特点进行跨域。
- location.hash + iframe:a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
- document.domain + iframe: 该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式,我们只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域,两个页面都通过js强制设置document.domain为基础主域,就实现了同域。