js之页面性能优化


使用webpack优化项目

1
2
3
4
对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
使用 ES6 引入模块并开启 tree shaking,这个技术可以移除没有使用的代码
优化图片,对于小图可以使用 base64 的方式(url-loader)写入文件中
开启SplitChunksPlugin按照路由拆分代码,实现按需加载和缓存

资源压缩合并、减少http请求

合并图片(css sprites)、CSS和JS文件合并、CSS和JS文件压缩。图片较多的页面也可以使用 lazyLoad 等技术进行优化。精灵图等

非核心代码异步加载

动态脚本加载

使用document.createElement创建一个script标签,即document.createElement(‘script’),然后把这个标签加载到body上面去。

defer(推荐)
1
2
3
4
5
6
7
8
// 在HTML解析完之后才会执行。如果是多个,则按照加载的顺序依次执行。
<script src="./defer1.js" defer></script>
<script src="./defer2.js" defer></script>
<script>console.log('同步任务')</script>

// 同步任务
// defer1
// defer2
async(不推荐)
1
2
3
4
5
6
7
8
// 在加载完之后立即执行(所以有可能阻碍dom渲染)。如果是多个,执行顺序和加载顺序无关。
<script src="./async1.js" async></script>
<script src="./defer1.js" defer></script>
<script src="./defer2.js" defer></script>
<script>console.log('同步任务')</script>

// 同步任务
// defer1、defer2的先后顺序不定

利用浏览器缓存(面试官问你:与浏览器缓存相关的http header有哪些?)

缓存:资源文件(比如图片)在本地的硬盘里存有副本,浏览器下次请求的时候,可能直接从本地磁盘里读取,而不会重新请求图片的url。
缓存分为强缓存和协商缓存
强缓存:不用请求服务区,直接使用本地的缓存。(可以查看百度官网)

强缓存

利用 http 响应头中的Expires或Cache-Control实现的。【重要】
浏览器第一次请求一个资源时,服务器在返回该资源的同时,会把上面这两个属性放在response header中。
注意:这两个response header属性可以只启用一个,也可以同时启用。当response header中,Expires和Cache-Control同时存在时,Cache-Control的优先级高于Expires。

1
2
3
4
5
6
7
8
9
1、Expires:服务器返回的绝对时间。

浏览器再次请求这个资源时,先从缓存中寻找,找到这个资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires的时间之前,就能命中缓存,否则就不行。
如果缓存没有命中,浏览器直接从服务器请求资源时,Expires Header在重新请求的时候会被更新。

缺点:由于Expires是服务器返回的一个绝对时间,存在的问题是:服务器的事件和客户端的事件可能不一致。在服务器时间与客户端时间相差较大时,缓存管理容易出现问题

2、Cache-Control:服务器返回的相对时间。
http1.1中新增的 response header。浏览器第一次请求资源之后,在接下来的相对时间之内,都可以利用本地缓存。超出这个时间之后,则不能命中缓存。重新请求时,Cache-Control会被更新。
协商缓存

浏览器发现本地有资源的副本,但是不太确定要不要使用,于是去问问服务器。当浏览器对某个资源的请求没有命中强缓存(也就是说超出时间了),就会发一个请求到服务器,验证协商缓存是否命中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、Last-Modified、If-Modified-Since。过程如下:
(1)浏览器第一次请求一个资源,服务器在返回这个资源的同时,会加上Last-Modified这个 response header,这个header表示这该资源在服务器上的最后修改时间。
(2)浏览器再次请求这个资源时,会加上If-Modified-Since这个 request header,这个header的值就是上一次返回的Last-Modified的值。
(3)服务器收到第二次请求时,会比对浏览器传过来的If-Modified-Since和资源在服务器上的最后修改时间Last-Modified,判断资源是否有变化。如果没有变化则返回304 Not Modified,但不返回资源内容
4)浏览器如果收到304的响应,就会从缓存中加载资源。

缺点:
如果服务器端在一秒内修改文件两次,但产生的Last-Modified却只有一个值。于是,下面这一对header出场了。

2、ETag、If-None-Match。过程如下:
(1)浏览器第一次请求一个资源,服务器在返回这个资源的同时,会加上ETag这个 response header,这个header是服务器根据当前请求的资源生成的唯一标识。这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间无关,所以也就很好地补充了Last-Modified的不足。
(2)浏览器再次请求这个资源时,会加上If-None-Match这个 request header,这个header的值就是上一次返回的ETag的值。
(3)服务器第二次请求时,会对比浏览器传过来的If-None-Match和服务器重新生成的一个新的ETag,判断资源是否有变化。如果没有变化则返回304 Not Modified,但不返回资源内容
(4)浏览器如果收到304的响应,就会从缓存中加载资源。

使用CDN

浏览器缓存始终只是为了提升二次访问的速度,对于首次访问的加速,我们需要从网络层面进行优化,最常见的手段就是CDN。

CDN是怎么做到加速的呢?

其实这是CDN服务商在全国各个省份部署计算节点,CDN加速将网站的内容缓存在网络边缘,不同地区的用户就会访问到离自己最近的相同网络线路上的CDN节点,当请求达到CDN节点后,节点会判断自己的内容缓存是否有效,如果有效,则立即响应缓存内容给用户,从而加快响应速度。如果CDN节点的缓存失效,它会根据服务配置去我们的内容源服务器获取最新的资源响应给用户,并将内容缓存下来以便响应给后续访问的用户。因此,一个地区内只要有一个用户先加载资源,在CDN中建立了缓存,该地区的其他后续用户都能因此而受益。

DNS预解析

通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。

第一步:打开或关闭DNS预解析
1
2
3
你可以通过在服务器端发送 X-DNS-Prefetch-Control 报头。或是在文档中使用值为 http-equiv 的meta标签:
<meta http-equiv="x-dns-prefetch-control" content="on">
注:在一些高级浏览器中,页面中所有的超链接(<a>标签),默认打开了DNS预解析。但是,如果页面中采用的https协议,很多浏览器是默认关闭了超链接的DNS预解析。如果加了上面这行代码,则表明强制打开浏览器的预解析。(如果你能在面试中把这句话说出来,则一定是你出彩的地方)
第二步:对指定的域名进行DNS预解析
1
2
3
如果我们将来可能从 smyhvae.com 获取图片或音频资源,那么可以在文档顶部的 标签中加入以下内容:
<link rel="dns-prefetch" href="http://www.smyhvae.com/">
当我们从该 URL 请求一个资源时,就不再需要等待 DNS 解析的过程。该技术对使用第三方资源特别有用。

预加载

在开发中,可能会遇到有些资源不需要马上用到,这时候就可以使用预加载。预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。

1
<link rel="preload" href="http://example.com" />

垃圾回收和内存泄漏

垃圾回收

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。所以才需要垃圾回收。
JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

垃圾回收机制怎么知道,哪些内存不再需要呢?
1
2
3
4
5
6
7
8
9
10
11
1、标记清除
这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
a++
var c = a + b
return c
}
哪些情况会引起内存泄漏?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1、意外的全局变量
function foo(arg) {
bar = "this is a hidden global variable";
}
// bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。

2、被遗忘的计时器或回调函数

3、闭包

4、清理的DOM元素需要删除引用
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
// 虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面。
垃圾回收的优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1、数组array优化:将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。
const arr = [1, 2, 3, 4];
console.log('浪里行舟');
arr.length = 0 // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。

2、对象尽量复用:对象尽量复用,尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为null,尽快被垃圾回收掉。
var t = {} // 每次循环都会创建一个新对象。
for (var i = 0; i < 10; i++) {
// var t = {};// 每次循环都会创建一个新对象。
t.age = 19
t.name = '123'
t.index = i
console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。

3、在循环中的函数表达式,能复用最好放到循环外面。