浏览器内部机制

浏览器内部机制

渲染机制

什么是DOCTYPE及作用

DTD是一系列的语法规则,用来定义XML或HTML的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式。

DOCTYPE是用来生命文档类型,一个主要的用途便是文件的合法性验证,。如果文件代码不合法那么浏览器解决便会出现一些差错

<!DOCTYPE html>

浏览器渲染过程

  1. 解析HTML文件,构建DOM树,同时浏览器主要负责下载CSS文件
  2. CSS文件下载完成,解析CSS文件形成树形的数据结构,结合DOM树合并成RenderObject树
  3. 布局(layout) RenderObject树,负责RenderObject树中的元素尺寸,位置等计算
  4. 绘制RenderObject树,绘制页面的像素信息
  5. 浏览器主进程将默认的图层和复合图层教给GPU进程,GPU进程再讲各个图层合成,最后显示页面

回流 Reflow

定义:

DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow

触发Reflow

  • 当你增加,删除,修改DOM节点,会导致Reflow
  • 当你移动DOM的位置,或者做动画的时候
  • 当你修改某些CSS样式的时候
  • 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候
  • 当你修改网页的默认字体时

重绘 Repaint

定义:

当页面中元素样式的改变并不影响他在文档流中的位置时(例如:color,background-color,visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称之为重绘

性能影响

回流比重绘的代价要更高

有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化:浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

当你访问以下属性或方法时,浏览器会立刻清空队列:

clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftwidth、heightgetComputedStyle()getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

如何避免

CSS

  • 避免使用table布局
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式
  • 将动画效果应用到position属性为absolute或fixed的元素上
  • 避免使用css表达式(例如calc())

Javascript

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性
  • 避免频繁操作DOM,创建一个documentFragment,在他上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置dispaly:none,操作结束再把它显示出来。因为在display尚需经为none的元素上进行的DOM操作不会引发回流和重绘
  • 对具有复杂动画的元素使用绝对定位,使他脱离文档流,否则会引发父元素及后续元素的频繁回流

JS运行机制

例题

console.log(1);
setTimeout(function(){
    console.log(3)
},0)
console.log(2); //1,3,2
console.log('A');
while(true){
    
}
console.log('B') //A 不会打印B,会在while中一直循环
console.log('A');
setTimeout(function(){
    console.log('B')
})
while(true){
    
} //A  不会打印B,会在while中一直循环

进程与线程

进程

进程是CPU资源分配的最小单位。可以理解为一个独立运行且拥有自己的资源空间的任务程序

进程包括运行中的程序和程序所使用到的内存和系统资源

线程

线程是CPU调度的最小单位。线程是建立在进程的基础上的一次程序运行单位,线程是程序中的一个执行流。一个进程可以有多个线程。

单线程和多线程

一个进程中只有一个执行流称作单线程,即程序执行时,所走的程序按照连续顺序下来,前面的必须处理好,后面的才会执行

一个进程中由于多个执行流成为多线程,即在一个程序中可以同时运行多个不同的进程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

JS为什么是单线程

JS的单线程,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是于用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时候浏览器无法做出抉择

事件循环(Event Loop)

JS分为同步任务和异步任务

同步任务都在主线程上执行,会形成一个执行栈主线程,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调。

一旦执行栈中所有同步任务执行完毕,系统就会读取任务队列,将可运行的异步任务添加到执行栈中,开始执行

let setTimeoutCallBack = function() {
  console.log('我是定时器回调');
};
let httpCallback = function() {
  console.log('我是http请求回调');
}

// 同步任务
console.log('我是同步任务1');

// 异步定时任务
setTimeout(setTimeoutCallBack,1000);

// 异步http请求任务
ajax.get('/info',httpCallback);

// 同步任务
console.log('我是同步任务2');

JS引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样反反复复就是我们所谓的事件循环(Event Loop)

img

异步任务

  • setTimeout和setInterval
  • DOM事件
  • ES6中的Promise

页面性能类

提升页面性能方法

  1. 资源压缩合并,减少HTTP请求
  2. 非核心代码异步加载
  3. 利用浏览器缓存
  4. 使用CDN
  5. 预解析DNS
非核心代码异步加载

异步加载的三种方式----async 和defer ,动态脚本创建

  1. async方法
  • async属性是HTML5新增属性,需要Chrome,FireFox,IE9+浏览器支持
  • async属性规定一旦脚本可用,则会异步执行
  • async属性仅适用于外部脚本
  • 如果是多个脚本,该方法不能保证脚本按顺序执行
<script type="text/javascript" src="xxxx.js" async="async"></script>	
  1. defer方法
  • 兼容所有浏览器
  • defer属性规定是否对脚本执行进行延迟,知道页面加载为止
  • 如果是多个脚本,该方法可以确保所有设置了defer属性的脚本按顺序执行
  • 如果脚本不会改变文档内容,可将defer属性加入到script标签中,以便加快文档处理速度
<script type="text/javascript" src="xxxx.js" defer></script>
  1. 动态创建script标签

通过window.onload方法确保页面加载完毕再将script标签插入到DOM中

function addScriptTag(src){  
    var script = document.createElement('script');  
    script.setAttribute("type","text/javascript");  
    script.src = src;  
    document.body.appendChild(script);  
}  
window.onload = function(){  
    addScriptTag("js/index.js");  
}  

异步加载的区别

  • defer是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行
  • async是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关

利用浏览器缓存

浏览器缓存类型:

  1. 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache 或from memory cache

相关的header

Expires: response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存。它的值为一个决定时间的GMT格式的时间字符串,比如Expires:thu,21 Jan 2018 23:39:02 GMT

Cache-Control: 这是一个相对时间,在配置缓存的时候,以秒为单位,用数值表示。当值设为max-age=300,则表示在这个请求正确返回时间的5分钟内再次加载资源,就会命中强缓存。比如Cache-Control:max-age=300

  1. 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源,另外协商缓存需要与cache-control共同使用

相关的header

last-Modified和if-modifed-since:当第一次请求资源时,服务器将资源传递给客户端时,会将资源最后更改的事件以"last-Modified:GTM"的形式加在实体首部上一起返回给客户端

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

客户端会为资源标记上该信息,下次再请求时,会把该信息附带在请求报文中一并带给服务器去检查,若传递时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有给修改过,直接返回304状态码,内容为空,这样就节省了传输数据量。如果两个时间不一致,则服务器会发回该资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小的多,这样就节省了网络带宽

但last-modified存在一些缺点:

  1. 某些服务端不能获取精确的修改时间
  2. 文件时间修改了,但文件内容却没有变

ETag 和if-none-match:Etag 是上一次加载资源时,服务器返回的response header是对该资源的一种唯一标识,只要资源发生变化浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的if-none-match里,服务器只需要比较客户端传来的if-None-Match跟自己服务器上该资源的Etag是否一致,就能判断资源相对客户端而言是否被修改过了,如果服务器发现ETag匹配不上,那么直接以常规GET200回包形式将新的资源发送给客户端;如果ETag是一致的,则直接返回304

使用CDN

通过将静态资源(例如JavaScript,css,图片等等)缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。

遍布全国的CDN节点和内容源示意图

预解析DNS

资源预解析是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。通过DNS预解析来告诉浏览器未来我们可能从某个特定的URL获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成DNS解析

<meta http-equiv="x-dns-prefetch-control" contrent="on">
//在http下,在大多数浏览器都支持a标签预解析,但是在https下,浏览器并不会直接开启a标签预解析
<link rel="dns-prefetch" href="//host_name_to_prefetch.com">
//一个页面中,存在多个域名的情况下,提升DNS预解析可以大大加快页面的性能

该技术对使用第三方资源特别有用,当浏览器真正请求该域中的某个资源时,DNS的解析就已经完成了,从而节省了宝贵的时间

错误监控类

JS异常处理

对于JavaScript而言,我们面对的仅仅只是异常,异常的出现不会直接导致JS引擎崩溃,最多只会使当前执行的任务终止。

  1. 当前代码块将作为一个任务压入任务队列中,JS线程会不断地从任务队列中提取任务执行。
  2. 当任务执行过程中出现异常,且异常没有捕获处理,则会一致沿着调用栈一层层向外抛出,最终终止当前任务的执行。
  3. JS线程会继续从任务中提取下一个任务继续执行

异常监控的处理方式

try-catch异常处理

try-catch处理异常的能力有限,只能捕获到运行时非异步错误,对于语法错误和异步错误就显得无能为力,捕捉不到。

try{
    error  //未定义变量
}catch(e){
    console.log('我知道错误了');
    console.log(e);
}

img

然而对于语法错误和异步错误就捕捉不到了

try{
    var error ='error';//中文分号
}catch(e){
    consol.log('我感知不到错误');
    console.log(e)
}

img

异步错误

try{
    setTimeout(()=>{
        error   //异步错误
    })
}catch(e){
    console.log('我感知不到错误');
    console.log(e)
}

img

window.onerror 异常处理

window.onerror捕获异常能力比try-catch稍微强点,无论是异步还是非异步错误,onerror都能捕获到运行时错误。

/**
* @msg 错误信息
* @url 出错文件
* @row 行号 
* @col 列号
* error 错误详细信息
*/
window.onerror = function(msg,url,row,col,error){
    console.log('我知道错误了');
    console.log({
        msg,url,row,col,error
    })
    return true
};
error;

img

异步错误

window.onerror = function (msg, url, row, col, error) {
  console.log('我知道异步错误了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
setTimeout(() => {
  error;
});

preview

然而window.onerror对于语法错误还是无能为力,所以我们在写代码的时候要尽可能避免语法错误。

在实例的使用过程中,onerror主要是来捕获预料之外的错误,而try-catch则是用来在可预见情况下监控特定的错误,两者结合使用更为高效

需要注意的是,window.onerror函数只有在返回true的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error.xxxx

img

关于window.onerror还有两点需要注意

  1. 对于onerror这种全局捕获,最好写在JS脚本的前面,因为你无法保证你写的代码是否出错,如果写在后面,一旦发生错误的话是不会被onerror捕获的。
  2. onerror是无法捕获到网络异常的错误

异常上报方式

  1. 通过Ajax发送数据
  2. 动态创建img标签的形式
function report(error){
    var reportUrl ='http://xxxx/report';
    new Image().src =reportUrl +'error'+error;
}

猜你喜欢

转载自www.cnblogs.com/lrgupup/p/12900252.html