一、渲染机制
- 对于渲染机制,会从
DOCTYPE
及作用、浏览器渲染过程、重排Reflow
、重绘Repaint
和 布局Layout
这几个方面。 - 对于
DOCTYPE
及作用,DTD
是文档类型定义,是一系列的语法规则,用来定义XML
或XHTML
的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式。DOCTYPE
是用来声明文档类型和DTD
规范的,一个主要的用途是文件的合法性验证。如果文件代码不合法,那么浏览器解析时便会出一些差错。 - 常见的
DOCTYPE
,如下所示:
HTML 5
,<!DOCTYPE html>
HTML 4.01 Strict
,这个DTD
包含所有HTML
元素和属性,但不包括表象或过时的元素(如font
),框架集是不允许的,<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
HTML 4.01 Transitional
,这个DTD
包含所有HTML
元素和属性,包括表象或过时的元素(如font
),框架集是不允许的,<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- 对于浏览器渲染过程,
HTML
按照HTML Parser
结合DOM
形成DOM Tree
,Style Sheets
按照CSS Parser
形成Style Rules
,DOM Tree
和Style Rules
形成Render Tree
渲染树,通过Layout
布局,Render Tree
计算每个节点的位置大小等信息,通过Painting
绘制根据计算好的信息绘制整个页面,最后Display
呈现出来。 - 解析
HTML
过程中的问题,自上而下解析HTML
,逐渐构建起DOM tree
,遇到style
、link
标签,会下载解析样式表,同时构建CSSOM tree
,不会阻塞html
的解析。但是遇到script
标签,它会立即下载并执行得到的脚本,会阻塞HTML
的解析。直到脚本里的同步代码部分(settimeout
等异步操作之外的代码)执行完之后,再接着解析接下来的HTML
。直到将整个HTML
文档的最后一个标签解析完毕,DOM tree
生成完毕。然后CSSOM tree
、render tree
生成,开始渲染。虽然script
的下载、执行均会阻塞HTML
的解析,但DOM tree
的生成是在文档的最后一个标签解析完之后才生成,那么script
标签的在HTML
中的位置对DOM tree
完全生成的时间应该是没有任何影响的。那为什么有把script
标签放在</body>
闭合之前的说法?
解答:这是因为实际上浏览器在正式渲染之前,会进行预渲染(first paint,怎么翻译的我忘了,反正可以理解为预渲染)。那么什么时候预渲染呢:当浏览器认为DOM tree生成得差不多了的时候。那什么是差不多呢,解析到body部分的第一个script标签之前。所以script如果放在head里,会推迟预渲染; 如果放在body的一开头,后面的一大堆标签还没解析,等于欺骗浏览器说已经“差不多了”,也就等于违背了设计预渲染的初衷,会造成整体渲染完毕的推迟。放在body中间也是一样的道理,所以还是放在最尾巴上比较好。
- 对于重排
Reflow
,DOM
结构中的各个元素都有自己的盒子模型,这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称为reflow
。 - 触发
reflow
的条件,如下所示:
- 当你增加、删除、修改
DOM
结点时,会导致Reflow
或Repaint
- 当你移动
DOM
的位置,或是搞个动画的时候 - 当你修改
CSS
样式的时候 - 当你
Resize
窗口的时候,但是移动端是没有这个问题,或是滚动的时候 - 当你修改网页的默认字体时
- 对于重绘
Repaint
,当各种盒子的时候、大小及其他的属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称为repaint
。 - 触发
repaint
的条件,如下所示:
DOM
改动CSS
改动
二、运行机制
- 对于
JS
的运行机制,分为JS
的单线程、任务队列和Event Loop
这几个方面。 - 对于
JS
的单线程,JavaScript
语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。javaScript
的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript
的主要用途是与用户互动,以及操作DOM
。这决定了它只能是单线程,否则会带来很复杂的同步问题。为了利用多核CPU
的计算能力,HTML5
提出Web Worker
标准,允许JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
。所以,这个新标准并没有改变JavaScript
单线程的本质。 - 对于任务队列,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
JS
引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。消息队列是消息队列是一个先进先出的队列,它里面存放着各种消息。事件循环是事件循环是指主线程重复从消息队列中取消息、执行的过程。 - 主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
JS
中分为两种任务类型:macrotask
和microtask
,在ECMAScript
中,microtask
称为jobs
,macrotask
可称为task
。macrotask
(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务。microtask
(又称为微任务),可以理解是在当前task
执行结束后立即执行的任务。宏任务就是说执行栈里的每一个被执行的代码就是一个宏任务,包括一个事件产生的回调执行,会在执行完毕一段代码之后先对dom
进行一次渲染,然后再执行下一个宏任务。微任务是再宏任务执行完毕之后立即执行的,他在dom
重新渲染之前,微任务的相应速度比宏任务是要快的。- 任务队列就是等候执行的一系列任务,总结如下:
- 任务队列又分为
macro-task
(宏任务)和micro-task
(微任务); macro-task
大概包括:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering;
micro-task
大概包括:process.nextTick,Promise,Object.observe(已废弃),MutationObserver(html5新特性)
setTimeout/Promise
等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。- 来自不同任务源的任务会进入到不同的任务队列
- 对于优先级,
micro-task > macro-task
。 - 对于
micro-task
:process.nextTick > Promise.then
- 对于
macro-task
:setTimeout > setImmediate
- 对于
Event Loop
事件循环机制,JS
的运行机制,如下所示:
- 执行栈执行宏任务
- 执行栈没有任务就去轮询事件队列
- 如果执行期间遇到微任务,就添加到微任务队列
- 一个宏任务执行完毕后会立即执行当前微任务队列的任务
- 宏任务执行完毕后开始渲染
- 然后开启下一轮宏任务
- 对于异步任务,常用的有,如下所示:
setTimeout
和setInterval
DOM
事件ES6
中的Promise
三、页面性能
- 题目:提升页面性能的方法有哪些。
- 对于提升页面性能,如下所示:
- 资源压缩合并,减少
HTTP
请求 - 非核心代码异步加载,异步加载的方式和异步加载的区别
- 利用浏览器缓存,缓存的分类和缓存的原理
- 使用
CDN
- 预解析
DNS
- 对于异步加载,异步加载的方式主要分为三种,动态脚本加载、
defer
和async
,它们之间的区别,如下所示:
defer
是在HTML
解析完以后才执行,如果是多个,按照加载的顺序依次执行async
是在加载完以后立即执行,如果是多个,执行顺序和加载顺序无关
- 对于浏览器缓存,主要分为强缓存和协商缓存。
- 强缓存,不会向服务器发送请求,直接从缓存中读取资源,在
hrome
控制台的Network
选项中可以看到该请求返回200
的状态码,并且Size
显示from disk cache
或from memory cache
。强缓存可以通过设置两种HTTP Header
实现:Expires
和Cache-Control
,Cache-Conctrol
的优先级比Expires
高。强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种,如下所示:
- 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)
- 存在该缓存结果和缓存标识,但是结果已经失效,强制缓存失效,则使用协商缓存
- 存在该缓存结果和缓存标识,且该结果没有还没有失效,强制缓存生效,直接返回该结果
Expires
和Cache-Control
,如下所示:
Expires
,缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。Expires=max-age + 请求时间
,需要和Last-modified
结合使用。Expires
是Web
服务器响应消息头字段,在响应http
请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires
是HTTP/1
的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示资源会在Wed, 22 Oct 2018 08:41:00 GMT
后过期,需要再次请求。Expires
是HTTP/1.0
控制网页缓存的字段,其值为服务器返回该请求的结果缓存的到期时间,即再次发送请求时,如果客户端的时间小于Expires
的值时,直接使用缓存结果。到了HTTP/1.1
,Expires
已经被Cache-Control
替代,原因在于Expires
控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,如果客户端与服务端的时间由于某些原因(时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存直接失效,那么强制缓存存在的意义就毫无意义。Cache-Control
,在HTTP/1.1
中,Cache-Control
是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300
时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5
分钟内再次加载资源,就会命中强缓存。Cache-Control
可以在请求头或者响应头中设置,并且可以组合使用多种指令,如下所示:public
:所有内容都将被缓存(客户端和代理服务器都可缓存)private
:所有内容只有客户端可以缓存,Cache-Control
的默认取值no-cache
:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定,设置了no-cache
之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致no-store
:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存max-age=xxx (xxx is numeric)
:缓存内容将在xxx
秒后失效max-stale
:能容忍的最大过期时间min-fresh
:能够容忍的最小新鲜度
Expires
和Cache-Control
两者对比,区别就在于Expires
是http1.0
的产物,Cache-Control
是http1.1
的产物,两者同时存在的话,Cache-Control
优先级高于Expires
;在某些不支持HTTP1.1
的环境下,Expires
就会发挥用处。所以Expires
其实是过时的产物,现阶段它的存在只是一种兼容性的写法。强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。- 协商缓存,协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,可以通过设置两种
HTTP Header
实现:Last-Modified
和ETag
,情况如下所示:
- 协商缓存生效,返回
304
和Not Modified
- 协商缓存失效,返回
200
和请求结果
Last-Modified
和If-Modified-Since
,如下所示:
- 浏览器在第一次访问资源时,服务器返回资源的同时,在
response header
中添加Last-Modified
的header
,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header
,如Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
- 浏览器下一次请求这个资源,浏览器检测到有
Last-Modified
这个header
,于是添加If-Modified-Since
这个header
,值就是Last-Modified
中的值;服务器再次收到这个资源请求,会根据If-Modified-Since
中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304
和空的响应体,直接从缓存读取,如果If-Modified-Since
的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200
Last-Modified
存在一些弊端,如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成Last-Modified
被修改,服务端不能命中缓存导致发送相同的资源。因为Last-Modified
只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源- 根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略,所以在
HTTP / 1.1
出现了ETag
和If-None-Match
ETag
和If-None-Match
,如下所示:
Etag
是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag
就会重新生成- 浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的
Etag
值放到request header
里的If-None-Match
里,服务器只需要比较客户端传来的If-None-Match
跟自己服务器上该资源的ETag
是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag
匹配不上,那么直接以常规GET 200
回包形式将新的资源(当然也包括了新的ETag
)发给客户端;如果ETag
是一致的,则直接返回304
知会客户端直接使用本地缓存即可
Last-Modified
与Etag
的比较,如下所示:
- 在精确度上,
Etag
要优于Last-Modified
,Last-Modified
的时间单位是秒,如果某个文件在1
秒内改变了多次,那么他们的Last-Modified
其实并没有体现出来修改,但是Etag
每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified
也有可能不一致 - 在性能上,
Eta
g要逊于Last-Modified
,毕竟Last-Modified
只需要记录时间,而Etag
需要服务器通过算法来计算出一个hash
值 - 在优先级上,服务器校验优先考虑
Etag
- 缓存机制,强制缓存优先于协商缓存进行,若强制缓存
(Expires和Cache-Control)
生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match)
,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200
,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304
,继续使用缓存。如果什么缓存策略都没设置,那么浏览器会采用一个启发式的算法,通常会取响应头中的Date
减去Last-Modified
值的10%
作为缓存时间。 - 实际场景应用缓存策略,如下所示:
- 频繁变动的资源,
Cache-Control: no-cache
。对于频繁变动的资源,首先需要使用Cache-Control: no-cache
使浏览器每次都请求服务器,然后配合ETag
或者Last-Modified
来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小 - 不常变化的资源,
Cache-Control: max-age=31536000
。通常在处理这类资源时,给它们的Cache-Control
配置一个很大的max-age=31536000
(一年),这样浏览器之后请求相同的URL
会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加hash
, 版本号等动态字符,之后更改动态字符,从而达到更改引用URL
的目的,让之前的强制缓存失效
- 用户行为对浏览器缓存的影响,用户在浏览器如何操作时,会触发怎样的缓存策略,如下所示:
- 打开网页,地址栏输入地址: 查找
disk cache
中是否有匹配。如有则使用;如没有则发送网络请求 - 普通刷新
(F5)
:因为TAB
并没有关闭,因此memory cache
是可用的,会被优先使用(如果匹配的话)。其次才是disk cache
- 强制刷新
(Ctrl + F5)
:浏览器不使用缓存,因此发送的请求头部均带有Cache-control: no-cache
(为了兼容,还带了Pragma: no-cache
),服务器直接返回200
和最新内容
四、错误监控
- 对于错误监控,分为前端错误的分类、错误的捕获方式和上报错误的基本原理。
- 对于前端错误的分类,主要是分为即时运行出现的代码错误和资源加载错误。
- 对于错误的捕获方式,如下所示:
- 即时运行错误的捕获方式,有
try...catch
、window.onerror
- 资源加载错误,有
object.onerror
、performance.getEntries()
、Error
事件捕获
- 问题:跨域的
JS
运行错误可以捕获吗,错误提示什么,应该怎么处理?
解答:可以捕获,错误信息为
Script error
,错误详情为null
,处理方式为 在script
标签增加crossorigin
属性,设置JS
资源响应头Access-Control-Allow-Origin
- 对于上报错误的基本原理,如下所示:
- 采用
Ajax
通信的方式上报 - 利用
Image
对象上报
- 错误监控的代码,如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>错误监控</title>
<script type="text/javascript">
window.addEventListener('error', function (e) {
console.log('捕获', e);
}, false);
</script>
</head>
<body>
<script src="//badu.com/test.js" charset="utf-8"></script>
<script type="text/javascript">
(new Image()).src = 'http://baidu.com/tesjk?r=tksjk';
</script>
</body>
</html>