本文首发于kmac007.me
资源压缩合并,减少HTTP请求
由于HTTP是无状态协议,意味着每次HTTP请求都需要建立通信链路、进行数据传输,而在服务器端,每个HTTP请求都需要启动独立的线程处理。这些通信和服务的开销是很昂贵的,减少HTTP请求的数目可有效提高访问性能。以下方法可以对资源进行压缩合并,减少HTTP请求:
- 合并CSS,并压缩
- 合并JavaScript,并压缩
- 图片压缩合并,通过CSS的操作偏移量显示不同的图片。(CSS Sprite,即俗称:雪碧图)
异步加载
异步加载的方式
- 动态脚本加载
通过JS动态的创建<script>
标签来动态加载js文件。 defer
<script src="./a.js" defer></script>
async
<script src="./a.js" async></script>
异步加载的区别
如果不设置defer
或者async
,那么浏览器在遇到<script>
标签时,文档的解析会停止,不再构建DOM,会导致页面阻塞直到脚本加载完毕。这是非常不好的用户体验,因此我们一般把<script>
标签放置在<body>
标签的最尾部。而defer
和async
两者同样可以解决这个问题。下面是二者的区别:
-
defer
是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行。 -
async
是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关。
浏览器缓存
缓存的分类
强缓存
Expires Expires:Sat, 13 May 2017 14:22:34 GMT
Cache-Control Cache-Control: max-age=3600
协商缓存
Last-Modified If-Modified-Since Last-Modified: Sat, 13 May 2017 14:22:34 GMT
Etag If-None-Match
对于缓存这个部分,打算再写一篇文章来描述。
使用CDN
CDN的全称是Content Delivery Network,即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。
简单来说,CDN将内容缓存到分布各地的CDN的节点上,根据用户的访问IP,使用户可就近取得所需要的内容,提高网络响应速度。
预解析DNS
DNS Prefetch
,即DNS的预获取,是前端优化的一部分。一般来说,在前端优化中与DNS有关的有两点;
- 减少DNS的解析次数
- DNS的预解析,即
DNS Prefetch
一次DNS解析一般要耗费20-120毫秒,减少DNS解析事件和次数是个很好的优化方式。默认情况下浏览器会对页面中和当前域名不在同一域的域名进行预解析,并且缓存结果,这就是隐式的DNS Prefetch
。如果向对页面中没有出现的域进行预解析,那么就要使用显式的DNS Prefetch
。
淘宝首页中的DNS Prefetch
使用方法:
DNS Prefetch
应该尽量放在网页的前面,推荐放在<meta>
后面,具体用法如下:
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//kmac007.me">
<link rel="dns-prefetch" href="//kmac007.com">
<link rel="dns-prefetch" href="//www.kmac007.com">
需要注意的是,虽然DNS Prefetch
能够加快页面的解析速度,但是不能滥用。
如果需要禁止隐式的DNS Prefetch
,可以使用以下的标签:
<meta http-equiv="x-dns-prefetch-control" content="off">
事件节流
比如,写一个滚动加载组件,监听scroll
事件,这时每次滚动都会执行多次回调函数,这是相当消耗性能的。因此,我们可以通过事件节流的方式,减少滚动回调函数的触发,从而提升性能。
例如:
var loadMore = document.getElementById('#loadMore')
var timer
//计时器的回调函数
function callback() {
//距离顶部的距离
const top = loadMore.getBoundingClientRect().top
//视口高度
const windowHeight = document.documentElement.clientHeight
if(top && top < windowHeight){
//loading...
}
}
//监听滚动事件并对函数节流
window.addEventListener('scroll', ()=>{
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(callback, 100)
})
如上代码,我们通过定时器的方式进行函数节流,有效减少了回调函数的执行。
减少DOM操作
对DOM操作的代价是高昂的,这在web应用中通常是一个性能瓶颈。
在《高性能JavaScript》中这么比喻:“把DOM看成一个岛屿,把JavaScript(ECMAScript)看成另一个岛屿,两者之间以一座收费桥连接”。所以每次访问DOM都会教一个过桥费,而访问的次数越多,交的费用也就越多。所以一般建议尽量减少过桥次数。
查询和修改DOM元素会造成页面的Repaint
和Reflow
。那么我们先了解下什么是Repaint
和Reflow
。
Repaint
和Reflow
Repaint
(重绘)就是在一个元素的外观被改变,但没有改变布局(宽高)的情况下发生,如改变visibility、outline、背景色等等。
Reflow
(重排) 的成本比 Repaint
的成本高得多的多。DOM Tree
里的每个结点都会有 Reflow
方法,一个结点的 Reflow
很有可能导致子结点,甚至父点以及同级结点的 Reflow
。在一些高性能的电脑上也许还没什么,但是如果 Reflow
发生在手机上,那么这个过程是非常痛苦和耗电的。
每次设置style属性改变节点样式,每设置一次都会导致一次reflow,所以最好通过设置class的方式; 有动画效果的元素,它的position属性应当设为fixed或absolute,这样不会影响其它元素的布局;如果功能需求上不能设置position为fixed或absolute,那么就权衡速度的平滑性。
总之,因为 Reflow 有时确实不可避免,所以只能尽可能限制Reflow的影响范围。
缓存DOM查询
// 未缓存 DOM 查询
for(let i = 0; i < document.getElementsByTagName('p').length; i++) {
//...
}
上面这种情况下,每次循环都要进行DOM查询,非常影响性能。
我们通过如下的方式缓存DOM,这样,就不需要每次都进行DOM查询,达到了减少DOM查询的目的。
// 缓存了 DOM 查询
var pList = document.getElementsByTagName('p')
for(let i = 0; i < pList.length; i++) {
//...
}
合并DOM插入
在循环插入DOM时,我们可以将部分生成的DOM节点插入到一个片段中,最后统一将片段插入到HTML中。
var listNode = document.getElementById('list')
// 要插入10个li
var frag = document.createDocumentFragment()
var li
for(let i = 0; i < 10; i++) {
li = document.createElement('li')
li.innderHTML = "List item" + i
frag.appendChild(li)
}
listNode.appendChild(frag)
懒加载
懒加载的原理是通过自定义属性标签存放图片原有的src属性,当img标签出现在浏览器窗口范围内再依次将原src属性填充以达到懒加载的效果。这种方法减少了开始加载网页时的请求,减少浏览器卡死的几率,减少了流量的消耗,同时提高了用户体验。
主要步骤:
- 判断图片是否可见(滚动高度 + 窗口高度 > 图片到页面顶部高度 && 图片到页面顶部高度 + 图片高度 > 滚动高度)
- 如果图片可见,将存放在
data-src
中原本的src属性填充src属性中。
以下为一个图片懒加载的示例:
<script async src="//jsfiddle.net/enpk4e92/embed/result,js,html,css/dark/"></script>
SSR服务端渲染
服务端渲染可以提高性能。React.js和Vue.js目前都支持服务端渲染。在此不做深究。