前言
随着计算机的发展,Web富应用时代的到来,Web 2.0早已不再是用div+css高质量还原设计的时代。自Gmail网页版邮件服务的问世开始,Web前端开发也开启了新的纪元。用户需求不断提高,各种新的技术层出不穷,前端工程师的地位也越来越重要。然而任何事物都是有两面性的,随着前端技术的发展,前端业务越来越繁重,这大大增加了JS代码量。因此,要提高Web的性能,我们不仅需要关注页面加载的时间,还要注重在页面上操作的响应速度。那么,接下来我们讨论几种能够提高JavaScript效率的方法。
一,浏览器应该有的功能
1,网络:
浏览器通过网络模块来下载各式各样的资源,例如html文本;javascript代码;样式表;图片;音视频文件等。
网络部分本质上十分重要,因为它耗时长,而且需要安全访问互联网上的资源。
2,资源管理:
从网络下载,或者本地获取到的资源需要有高效的机制来管理它们。
例如如何避免重复下载,资源如何缓存等
3,网页浏览:
这是浏览器的核心也是最基本的功能,最重要的功能。
如何将资源转变为可视化的结果。
多页面管理
插件与管理
账户和同步
安全机制
开发者工具
浏览器的主要功能总结起来就是一句话:将用户输入的url转变成可视化的图像。
1.从url到DOM树
2.从DOM树到可视化图像
这两个过程之间的关系并没有那么明确,我们可以统称这两个过程为页面的渲染
4,浏览器的内核(渲染引擎)
在浏览器中有一个最重要的模块,它主要的作用是将页面转变为可视化的图像结果。
这个模块就是浏览器内核,通常它也被称为渲染引擎。
IE---------->Trident
Safari------>WebKit
WebKit本身主要是由两个引擎构成的,
- 一个正是渲染引擎“WebCore”,
- 另一个则是javascript解释引擎“JSCore”,
它们均是从KDE的渲染引擎KHTML及javascript解释引擎KJS衍生而来。
Chrome------>WebKit的分支引擎----->Blink
13年发布的Chrome 28.0.1469.0版本开始,Chrome放弃Chromium引擎转
而使用最新的Blink引擎(基于WebKit2——苹果公司于2010年推出的新的WebKit引擎),
Blink对比上一代的引擎精简了代码、改善了DOM框架,也提升了安全性。
Opera
旧版Opera 4至6版本 :Elektra排版引擎
Opera7.0 :Presto渲染引擎
Opera在2013年2月宣布放弃Presto:
采用Chromium引擎;
又转为Blink引擎;
Firefox------>Gecko
5,进程与线程
进程: 程序的一次执行, 它占有一片独有的内存空间.是操作系统执行的基本单元。
一个进程中至少有一个运行的线程: 主线程, 进程启动后自动创建
一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
一个进程内的数据可以供其中的多个线程直接共享,多个进程之间的数据是不能直接共享的
线程:是进程内的一个独立执行单元,是CPU调度的最小单元。程序运行的基本单元
线程池(thread pool): 保存多个线程对象的容器, 实现线程对象的反复利用
JS引擎是单线程运行的!(回忆事件轮询机制)
6,现代浏览器:多进程、多线程模型
1.不堪回首的过去:
当你通过浏览器打开很多页面的时候,如果其中一个页面不响应了或者崩溃了,
那么随之而来的将会是更不幸的事情,你开打的所有页面都会得不到响应,
最让人不能忍受的是,其中的一些页面可能还包含了未保存或者未发送的信息
2.浏览器产商如何解决
采用多进程模型,该模型可以带来的好处
①.避免因单个页面的不响应或者崩溃影响整个浏览器的稳定性
②.当第三方插件崩溃时,也不会影响整个浏览器的稳定性
③.安全
3.浏览器到底有些什么进程
①.Browser进程:
浏览器的主进程,负责浏览器界面的显示,和各个页面的管理,
浏览器中所有其他类型进程的祖先,负责其他进程的的创建和销毁
它有且只有一个!!!!!
②.Renderer进程:
网页渲染进程,负责页面的渲染,可以有多个
当然渲染进程的数量不一定等于你开打网页的个数
③.各种插件进程:
④.GPU进程:
移动设备的浏览器可能不太一样:
Android不支持插件,所以就没有插件进程
GPU演化成了Browser进程的一个线程
Renderer进程演化成了操作系统的一个服务进程,它仍然是独立的
4.每个进程内部又有很多线程
多线程的目的主要是保持用户界面的高度响应
例如:为了不让Browser进程的UI线程被其他耗时的操作(数据库读写,本地文件读写)所阻塞,
那么我们就把这些操作放到分线程中去处理在Renderer进程中,为了不让其他操作阻止 渲染线程的高速执行,我们通常会将渲染过程【管线化】,
利用计算机的多核优势,让渲染的不同阶段在不同的线程中执行
二,浏览器渲染引擎
1, 主要模块
一个渲染引擎主要包括:HTML解析器,CSS解析器,javascript引擎,布局layout模块,绘图模块。
- HTML解析器:解释HTML文档的解析器,主要作用是将HTML文本解释成DOM树。
- CSS解析器:它的作用是为DOM中的各个元素对象计算出样式信息,为布局提供基础设施
- Javascript引擎:使用Javascript代码可以修改网页的内容,也能修改css的信息,javascript引擎能够解释javascript代码,并通过DOM接口和CSS树接口来修改网页内容和样式信息,从而改变渲染的结果。
- 布局(layout):在DOM创建之后,Webkit需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达这所有信息的内部表示模型
- 绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果
备注:文档对象模型(Document Object Model,简称DOM)
2,渲染过程
浏览器渲染页面的整个过程:浏览器会从上到下解析文档。
- 遇见 HTML 标记,调用HTML解析器解析为对应的 token (一个token就是一个标签文本的序列化)并构建 DOM 树(就是一块内存,保存着tokens,建立它们之间的关系)。
- 遇见 style/link 标记 调用解析器 处理 CSS 标记并构建 CSS样式树。
- 遇见 script 标记 调用 javascript解析器 处理script标记,绑定事件、修改DOM树/CSS树 等
- 将 DOM树 与 CSS树 合并成一个渲染树。
- 根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖图形库)。
- 将各个节点绘制到屏幕上。
*以上这些模块依赖很多其他的基础模块,包括要使用到网络 存储 2D/3D图像 音频视频解码器 和 图片解码器。
*所以渲染引擎中还会包括如何使用这些依赖模块的部分。
三, 阻塞渲染
1,关于css阻塞:
声明:只有link引入的外部css才能够产生阻塞。
1.style标签中的样式:
(1). 由html解析器进行解析;
(2). 不阻塞浏览器渲染(可能会产生“闪屏现象”);
(3). 不阻塞DOM解析;
2.link引入的外部css样式(推荐使用的方式):
(1). 由CSS解析器进行解析。
(2). 阻塞浏览器渲染(可以利用这种阻塞避免“闪屏现象”)。
(3). 阻塞其后面的js语句的执行:
(4). 不阻塞DOM的解析:
3.优化核心理念:尽可能快的提高外部css加载速度
(1).使用CDN节点进行外部资源加速。
(2).对css进行压缩(利用打包工具,比如webpack,gulp等)。
(3).减少http请求数,将多个css文件合并。
(4).优化样式表的代码
2.关于js阻塞:
1.阻塞DOM解析:
原因:浏览器不知道后续脚本的内容,如果先去解析了下面的DOM,而随后的js删除了后面所有的DOM,
那么浏览器就做了无用功,浏览器无法预估脚本里面具体做了什么操作,例如像document.write这种操作,索性全部停住,等脚本执行完了,浏览器再继续向下解析DOM。
2.阻塞页面渲染:
原因:js中也可以给DOM设置样式,浏览器同样等该脚本执行完毕,再继续干活,避免做无用功。
3.阻塞后续js的执行:
原因:维护依赖关系,例如:必须先引入jQuery再引入bootstrap
3.备注
【备注1】:css的解析和js的执行是互斥的(互相排斥),css解析的时候js停止执行,js执行的时候css停止解析。
【备注2】:无论css阻塞,还是js阻塞,都不会阻塞浏览器加载外部资源(图片、视频、样式、脚本等)
原因:浏览器始终处于一种:“先把请求发出去”的工作模式,只要是涉及到网络请求的内容,
无论是:图片、样式、脚本,都会先发送请求去获取资源,至于资源到本地之后什么时候用,
由浏览器自己协调。这种做法效率很高。
【备注3】:WebKit 和 Firefox 都进行了【预解析】这项优化。在执行js脚本时,浏览器的其他线程会解析文档的其余部分,
找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,
从而提高总体速度。请注意,预解析器不会修改 DOM 树
*在上述的过程中,网页在加载和渲染过程中会触发“DOMContentloaded”和“onload”事件
>分别是在DOM树构建(解析)完成之后,以及DOM树构建完并且网页所依赖的资源都加载完之后
* 上面介绍的是一个完整的渲染过程,但现代网页很多都是动态的,这意味着在渲染完成之后,由于网页的动画或者用户的交互,
浏览器其实一直在不停地重复执行渲染过程。(重绘重排),以上的数字表示的是基本顺序,这不是严格一致的,
这个过程可能重复也可能交叉
四,验证css和js阻塞
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
div {
width: 400px;
height: 400px;
background: red;
}
</style>
<!--1.link进来的css阻塞页面渲染-->
<link rel="stylesheet" href="/css/sleep-3000-common.css">
<!--2.defer的作用:页面解析完触发-->
<script defer type="text/javascript" src="/js/logDiv.js"></script>
<!--3.link进来的css不阻塞页面解析-->
<link rel="stylesheet" href="/css/sleep-3000-common.css">
<script defer type="text/javascript" src="/js/logDiv.js"></script>
<!--4.js阻塞页面解析-->
<script type="text/javascript" src="/js/block.js"></script>
<script defer type="text/javascript" src="/js/logDiv.js"></script>
<!--5.js阻塞页面渲染-->
<script type="text/javascript" src="/js/block.js"></script></head><body>
<div></div>
</body>
</html>
const Koa = require('koa');
const app = new Koa();
const serve = require('koa-static');
const fs = require('fs');
const path = require('path');
function sleep(time) {
return new Promise(function(res) {
setTimeout(() => {
res()
}, time);
}
)}
app.use(serve(__dirname + '/static/html'));
app.use(async (ctx) => {
let { url } = ctx.request;
const _timeArr = url.match(/sleep-(\d+)(?=-)/);
let time = null;
if (_timeArr) {
url = url.replace(/sleep-\d+-/, '');
time = _timeArr[1];
}
const res = await new Promise(function(res, rej) {
fs.readFile(path.join(__dirname, 'static', url), (err, data) => {
if (err) return rej(err);
res(data);
})
});
if (/css/.test(url)) {
ctx.response.set('Content-Type', 'text/css');
} if (time) {
await sleep(time);
}
ctx.body = res;
});
app.listen(3000, () => console.log(`服务器成功启动,访问地址是:http://localhost:3000/index.html`));
1,验证link进来的css阻塞页面渲染
// sleep-6000-common.css经过服务器处理会延长3000ms 内容:
<link rel="stylesheet" href="/css/sleep-3000-common.css">
sleep-6000-common.css的内容:
div {
background: lightblue;
}
3000ms后 :

结论:link进来的css阻塞页面渲染
2,验证defer的作用
不加defer
<script type="text/javascript" src="/js/logDiv.js"></script>
logDiv.js的内容:
const div = document.querySelector('div');console.log(div);

拿不到当前的div,因为加载logDiv.js的时候页面还并没有渲染完
加入defer<script defer type="text/javascript" src="/js/logDiv.js"></script>

结论:defer的作用是文档加载完毕了再执行脚本,这样会避免找不到对象的问题。
3.link进来的css不阻塞页面解析
<link rel="stylesheet" href="/css/atguigu-sleep-3000-common.css">
<script defer type="text/javascript" src="/js/logDiv.js"></script>

通过前面的sleep-3000-common.css我们知道,他会阻塞页面渲染3000ms,但此时logDiv.js却在他渲染出来之前就打印了div,
结论:link进来的css不阻塞页面解析
4,js阻塞页面解析
<script type="text/javascript" src="/js/block.js"></script><script defer type="text/javascript" src="/js/logDiv.js"></script>
block.js内容:
const arr = [];for (let i = 0; i < 100000000; i++) { arr.push(i); arr.splice(i % 3, i % 7, i % 5);}console.log(1111);

很明显,block.js的循环非常消耗性能和时间,但logDiv.js还是要等他执行完后才输出,只要block.js不执行完,后面的代码就不会执行,只能等着。
结论:js阻塞页面解析
5,js阻塞页面渲染
<script type="text/javascript" src="/js/block.js"></script>

由于block.js执行时间太长,连页面都加载不出来
结论:js阻塞页面渲染
五,重绘,重排
重绘(Repaint)
重绘是一个元素外观的改变所触发的浏览器行为,例如改变outline、背景色等属性。浏览器会 根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,所以并不一定伴随重排。
需要注意的是:重绘是以图层为单位,如果图层中某个元素需要重绘,那么整个图层都需要重绘。比如一个图层包含很多节点,其中有个gif图,gif图的每一帧,都会重回整个图层的其他节点,然后生成最终的图层位图。所以这需要通过特殊的方式来强制gif图属于自己一个图层(translateZ(0)或者translate3d(0,0,0)CSS3的动画也是一样(好在绝大部分情况浏览器自己会为CSS3动画的节点创建图层)
重排(Reflow 回流)
渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排"重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。但是,"重排"必然导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。
触发重绘的属性
* color * background * outline-color
* border-style * background-image * outline
* border-radius * background-position * outline-style
* visibility * background-repeat * outline-width
* text-decoration * background-size * box-shadow
###触发重排(回流)的属性
盒子模型相关属性会触发重布局 定位属性及浮动也会触发重布局: 改变节点内部文字结构也会触发重布局:
* width * top * text-align
* height * bottom * overflow-y
* padding * left * font-weight
* margin * right * overflow
* display * position * font-family
* border-width * float * line-height
* border * clear * vertival-align
* min-height * white-space
常见的触发重排的操作
Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,
一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,
但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。
所以,下面这些动作有很大可能会是成本比较高的。
当你增加、删除、修改 DOM 结点时,会导致 Reflow , Repaint。
当你移动 DOM 的位置
当你修改 CSS 样式的时候。
当你 Resize 窗口的时候(移动端没有这个问题,因为移动端的缩放没有影响布局视口)
当你修改网页的默认字体时。
获取某些属性时(width,height...)
注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。
优化方案
如果我们需要使得动画或其他节点渲染的性能提高,需要做的就是减少浏览器在运行时所需要做的工作(尽量减少1234步)
1. 计算需要被加载到节点上的样式结果(Recalculate style--样式重计算)
2. 为每个节点生成图形和位置(Layout--回流和重布局)
3. 将每个节点填充到图层中(Paint Setup和Paint--重绘)
4. 组合图层到页面上(Composite Layers--图层重组)
1.元素位置移动变换时尽量使用CSS3的transform来代替对top left等的操作
变换(transform)和透明度(opacity)的改变仅仅影响图层的组合
2.使用opacity来代替visibility
(1).使用visibility不触发重排,但是依然重绘。
(2).直接使用opacity即触发重绘,又触发重排(GPU底层设计如此!)。
(3).opacity配合图层使用,即不触发重绘也不触发重排。
原因:
透明度的改变时,GPU在绘画时只是简单的降低之前已经画好的纹理的alpha值来达到效果,并不需要整体的重绘。
不过这个前提是这个被修改opacity本身必须是一个图层。
3.不要使用table布局table-cell
4.将多次改变样式属性的操作合并成一次操作
不要一条一条地修改DOM的样式,预先定义好class,然后修改DOM的className
5.将DOM离线后再修改由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。
6.利用文档碎片(documentFragment)------vue使用了该种方式提升性能。
7.不要把获取某些DOM节点的属性值放在一个循环里当成循环的变量
当你请求向浏览器请求一些 style信息的时候,就会让浏览器flush队列,比如:
1. offsetTop, offsetLeft, offsetWidth, offsetHeight
2. scrollTop/Left/Width/Height
3. clientTop/Left/Width/Height
4. width,height
当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要刷新内部队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。
8.动画实现过程中,启用GPU硬件加速:transform: tranlateZ(0)
9.为动画元素新建图层,提高动画元素的z-index
六,CDN
网站通常将其所有的服务器都放在同一个地方,当用户群增加时,公司就必须在多个地理位置不同的服务器上部署内容为了缩短http请求的时间,我们应该把大量的静态资源放置的离用户近一点。
基本思路:
尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
基础架构:最简单的CDN网络由一个DNS服务器和几台缓存服务器组成
1.当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。
2.CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。
3.用户向CDN的全局负载均衡设备发起内容URL访问请求。
4.CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。
5.区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。
6.全局负载均衡设备把服务器的IP地址返回给用户。
7.用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。
如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,
那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。
七,浏览器本地存储
Cookie, SessionStorage, LocalStorage这三者都可以被用来在浏览器端存储数据,而且都是字符串类型的键值对!
cookie 是什么?
cookie翻译过来是小甜饼的意思,是网景公司的前雇员 Lou Montulli 在1993年3月的发明的。cookie是纯文本格式,不包含任何可执行的代码信息,伴随着用户请求在 Web 服务器和浏览器之间传递。cookie本质上属于http的范畴,因为http协议本身是无状态的,服务端是没有办法区分请求来自于哪个客户端,即便是来自于同一个客户端的多次请求 我们的服务端也是没有能力来区分的。就是因为http协议是无状态的,所以才需要cookie去维持客户端的状态。
cookie的生成方式:
1.客户端生成:
在 JavaScript 中通过 document.cookie 属性,你可以创建、维护和删除 cookie设置 document.cookie 属性的值并不会删除存储在页面中的所有 cookie。它只简单的创建或修改字符串中指定的 cookie要使用 JavaScript 提取 cookie 的值,只需要document.cookie 中读取即可
2.服务端生成:
Web 服务器通过发送一个称为 Set-Cookie 的 HTTP消息头来创建一个 cookie HttpOnly为避免跨域脚本 (XSS) 攻击,通过JavaScript的 Document.cookie API无法访问有HttpOnly 标记的Cookie,
cookie的缺点
安全性 :由于cookie在HTTP中是明文传递的,其中包含的数据都可以被他人访问,可能会被篡改、盗用。
大小限制: cookie的大小限制在4KB左右,若要做大量存储显然不是理想的选择。
增加流量: cookie每次请求都会被自动添加到Request Header中,无形中增加了流量。
cookie信息越大,对服务器请求的时间也越长。因此要慎用cookie,不要在cookie中存储重要和敏感的数据。
1.静态资源是不会携带cookie的
2.cookie一般都是后台种的,很少让前端来直接写
3.cookie分:持久级别、session级别
4.cookie一般用于和session通信
八,缓存机制
1. 缓存理解
1. 缓存定义:
1. 浏览器在本地磁盘上将用户之前请求的数据存储起来,当访问者再次需要改数据的时候无需再次发送请求,直接从浏览器本地获取数据
2. 缓存的好处:
1. 减少请求的个数
2. 节省带宽,避免浪费不必要的网络资源
3. 减轻服务器压力
4. 提高浏览器网页的加载速度,提高用户体验
2. 缓存分类
1. 强缓存
1. 不会向服务器发送请求,直接从本地缓存中获取数据
2. 请求资源的的状态码为: 200 ok(from memory cache)
2. 协商缓存
1. 向服务器发送请求,服务器会根据请求头的资源判断是否命中协商缓存
2. 如果命中,则返回304状态码通知浏览器从缓存中读取资源
3. 强缓存 & 协商缓存的共同点
1. 都是从浏览器端读取资源
4. 强缓存 VS 协商缓存的不同点
1. 强缓存不发请求给服务器
2. 协商缓存发请求给服务器,根据服务器返回的信息决定是否使用缓存