减少HTTP请求数量
合并脚本和样式表
使用CSS雪碧图
减小请求带宽
使用Gzip
gzip能够压缩任何一个文本类型的响应,包括html,xml,json。大大缩小请求返回的数据量。
压缩js和css
减小cookie大小
利用缓存
添加Expires头部
Exipres是用来设置文件的过期时间的,一般对css、js、图片资源有效。 他可以使内容具有缓存性,这样下回再访问同样的资源时就通过浏览器缓存区读取,不需要再发出http请求。
使用外部js和css文件
目的是可以利用缓存
配置Etag
它用来判断浏览器缓存里的元素是否和原来服务器上的一致。使用ETags减少Web应用带宽和负载
页面结构
css放在头部,js放在底部
大多数人已经知道通常要把 JavaScript 放在文档底部,把 CSS 放在文档顶部。为什么呢?因为 JavaScript 会阻塞页面的解析,而外部样式表会阻塞页面的呈现和 JavaScript 的执行。
CSS阻塞渲染
通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。
但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。JavaScript 阻塞文档解析
当在 HTML 文档中遇到 script 标签后控制权将交给 JavaScript,在 JavaScript 下载并执行完成之前,都不会解析 HTML。因此如果将 JavaScript 放在文档顶部,恰好这个时候 JavaScript 脚本加载的特别慢,用户将会等待很长一段时间,这段个时候 HTML 文档还没有解析到 body 部分,页面会是空白的。
其他
减少DOM操作次数
- 为什么DOM很慢?
谈到这里需要对浏览器利用 HTML/CSS/JavaScript 等资源呈现出精彩的页面的过程进行简单说明。
- 浏览器在收到 HTML 文档之后会对文档进行解析开始构建 DOM (Document Object Model) 树
- 进而在文档中发现样式表,开始解析 CSS 来构建 CSSOM(CSS Object Model)树
- 这两者都构建完成后,开始构建渲染树。
在每次修改了 DOM 或者其样式之后都要进行 DOM树的构建,CSSOM 的重新计算,进而得到新的渲染树。浏览器会利用新的渲染树对页面进行重排和重绘,以及图层的合并。通常浏览器会批量进行重排和重绘,以提高性能。但当我们试图通过 JavaScript 获取某个节点的尺寸信息的时候,为了获得当前真实的信息,浏览器会立刻进行一次重排。
这就是为什么减少创建集中的DOM节点以及快速注入是那么的重要了。现在假设我们页面中有一个<ul>
元素,调用ajax获取JSON列表,然后使用javascript更新元素内容。通常,程序员会这么写:
var list = document.querySelector('ul');
ajaxResult.items.forEach(function(item) {
// 创建<li>元素
var li = document.createElement('li');
li.innerHTML = item.text;
// <li>元素常规操作,例如添加class,更改属性attribute,添加事件监听等
// 迅速将<li>元素注入父级<ul>中
list.apppendChild(li);
});
上面的代码其实是一个错误的写法,将<ul>
元素带着对每一个列表的DOM操作一起移植是非常慢的。如果你真的想要 使用document.createElement,并且将对象当做节点来处理,那么考虑到性能问题,你应该使用DocumentFragement。
DocumentFragement 是一组子节点的“虚拟存储”,并且它没有父标签。在我们的例子中,将DocumentFragement想象成看不见的<ul>
元素,在 DOM外,一直保管着你的子节点,直到他们被注入DOM中。那么,原来的代码就可以用DocumentFragment优化一下:
var frag = document.createDocumentFragment();
ajaxResult.items.forEach(function(item) {
// 创建<li>元素
var li = document.createElement('li');
li.innerHTML = item.text;
// <li>元素常规操作
// 例如添加class,更改属性attribute,添加事件监听,添加子节点等
// 将<li>元素添加到碎片中
frag.appendChild(li);
});
// 最后将所有的列表对象通过DocumentFragment集中注入DOM
document.querySelector('ul').appendChild(frag);
为DocumentFragment追加子元素,然后再将这个DocumentFragment加到父列表中,这一系列操作仅仅是一个DOM操作,因此它比起集中注入要快很多。
如果你不需要将列表对象当做节点来操作,更好的方法是用字符串构建HTML内容:
var htmlStr = '';
ajaxResult.items.forEach(function(item) {
// 构建包含HTML页面内容的字符串
htmlStr += '<li>' + item.text + '</li>';
});
// 通过innerHTML设定ul内容
document.querySelector('ul').innerHTML = htmlStr;
这当中也只有一个DOM操作,并且比起DocumentFragment代码量更少。在任何情况下,这两种方法都比在每一次迭代中将元素注入DOM更高效。
高频执行事件/方法的防抖
通常,开发人员会在有用户交互参与的地方添加事件,而往往这种事件会被频繁触发。想象一下窗口的resize事件或者是一个元素的onmouseover事件 - 他们触发时,执行的非常迅速,并且触发很多次。如果你的回调过重,你可能使浏览器死掉。这就是为什么我们要引入防抖。
防抖可以限制一个方法在一定时间内执行的次数。以下代码是个防抖示例:
// 取自 UnderscoreJS 实用框架
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
// 添加resize的回调函数,但是只允许它每300毫秒执行一次
window.addEventListener('resize', debounce(function(event) {
// 这里写resize过程
}, 300));
debounce方法返回一个方法,用来包住你的回调函数,限制他的执行频率。使用这个防抖方法,就可以让你写的频繁回调的方法不会妨碍用户的浏览器!
网络存储的静态缓存和非必要内容优化
web Storage的API曾经是Cookie API一个显著的进步,并且为开发者使用了很多年了。这个API是合理的,更大存储量的,而且是更为健全理智的。一种策略是去使用Session存储来存储非必要的,更为静态的内容,例如侧边栏的HTML内容,从Ajax加载进来的文章内容,或者一些其他的各种各样的片断,是我们只想请求一次的。
现在同样的内容不会被重复请求,你的应用运行的更加有效。花一点儿时间,看看你的网站设计,将那些不会变化,但是会被不断请求的内容挑出来,你可以使用Web Storage工具来提升你网站的性能。
使用异步加载,延迟加载依赖
RequireJS已经迎来了异步加载和AMD格式的巨大浪潮。XMLHttpRequest(该对象可以调用AJAX)使得资源的异步加载变得流行起来,它允许无阻塞资源加载,并且使 onload 启动更快,允许页面内容加载,而不需要刷新页面。
使用Array.prototype.join代替字符串连接
有一种非常简单的客户端优化方式,就是用Array.prototype.join代替原有的基本的字符连接的写法。在上面的“最佳实践1”中,我在代码中使用了基本字符连接:
htmlStr += '<li>' + item.text + '</li>';
但是下面这段代码中,我用了优化:
var items = [];
ajaxResult.items.forEach(function(item) {
// 构建字符串
items.push('<li>', item.text, '</li>');
});
// 通过innerHTML设置列表内容
document.querySelector('ul').innerHTML = items.join('');
使用事件委托
想象一下,如果你有一个无序列表,里面有一堆
- 元素,每一个
- 元素都会在点击的时候触发一个行为。这个时候,你通常会在每一个元素上添加一个事件监听,但是如果当这个元素或者你添加了监听的这个对象会被频繁的移除添加呢?这个时候,你在移除添加元素的同时需要处理事件监听的移除和添加。这个时候,我们就需要引入事件委托了。事件委托是在父级元素上添加一个事件监听,来替代在每一个子元素上添加事件监听。当事件被触发时,event.target会评估相应的措施是否需要被执行。下面我们给出了一个简单的例子:
-
// 获取元素,添加事件监听 document.querySelector('#parent-list').addEventListener('click', function(e) { // e.target 是一个被点击的元素! // 如果它是一个列表元素 if(e.target && e.target.tagName == 'LI') { // 我们找到了这个元素,对他的操作可以写在这里。 } });
上面的例子是不可思议的简单,当事件发生的时候,它没有轮询父节点去寻找匹配的元素或选择器,且它不支持基于选择器的查询(例如用class name,或者id来查询)。所有的JavaScript框架提供了委托选择器匹配。重点是,你避免了为每一个元素加载事件监听,而是在父元素上加一个事件监听。这样大大的增加了效率,并且减少了很多维护!
使用Data URI代替图片SRC
提升页面大小的效率,不仅仅是取决于使用精灵或是压缩代码,给定页面的请求数量在前端性能中也占有了很不小的重量。减少请求可以让你的网站加载更快,而其中一种减少页面请求的方法就是用Data URI代替图片的src属性:
<!-- 以前的写法 --> <img src="/images/logo.png" /> <!-- 使用data URI的写法 --> <img src="data: image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fH ...." />
当然页面大小会增加(如果你的服务器使用适当的gzip内容,这个增加会很小),但是你减少了潜在的请求,同时也在过程中减少了服务器请求的数量。现在大多数浏览器都支持Data URI,在CSS中的背景骨片也可以使用Data URI,因此这个策略现在已经可以在应用层级,广泛应用。
使用媒体查询加载指定大小的背景图片
直到CSS @supports被广泛支持,CSS媒体查询的使用接近于CSS中写逻辑控制。我们经常用CSS媒体查询来根据设备调整CSS属性(通常根据屏幕宽度调整CSS属性),例如根据不同的屏幕宽度来设置不同的元素宽度或者是悬浮位置。那么我们为什么不用这种方式来改变背景图片呢?
/* 默认是为桌面应用加载图片 */ .someElement { background-image: url(sunset.jpg); } @media only screen and (max-width : 1024px) { .someElement { background-image: url(sunset-small.jpg); } }
上面的代码片段是为手机设备或是类似的移动设备加载一个较小尺寸的图片,特别是需要一个特别小的图片时(例如图片的大小几乎不可视)。
控制DOM大小
这一篇中,我们要说如何控制DOM的大小,来优化前端性能。DOM很慢是众所周知的,使得网站变慢的罪魁祸首是大量的DOM。想象一下,假如你有一个有着上千节点的DOM,在想象一下,使用querySelectorAll或者getElementByTagName,或者是其他以DOM为中心的搜索方式来搜索一个节点,即使是使用内置方法,这也将是一个非常费力的过程。你要知道,多余的DOM节点会使其他的实用程序也变慢的。
我见过的一种情况,DOM的大小悄然增加,是在一个AJAX网站,它将所有的页面都存在了DOM中,当一个新的页面通过AJAX被加载时,旧的页面就会被存入隐藏的DOM节点。对于DOM的速度,将有灾难性的降低,特别是当一个页面是动态加载的。所以你需要一种更好的方法。在这种情况下,当页面是通过AJAX加载的,并且以前的页面是存储在客户端的,最好的方法就是将内容通过String HTML存储(将内容从DOM中移除),然后使用事件委托来避免特定元素事件。这么做的同时,当在客户端缓存内容的时候,可以避免大量的DOM生成。
通常控制DOM大小的技巧包括:
使用:before和:after伪元素
延迟加载和呈现内容
使用事件委托,更简便的将节点转换成字符串存储
简单一句话:尽量使你的DOM越小越好。链接CSS,避免使用@import
有时候,@import太好用以至于很难抗拒它的诱惑,但是为了减少令人抓狂的请求,你必须要拒绝它!最常见的用法是在一个”main”CSS文件中,没有任何的内容,只有@import规则。有时,多个@import规则往往会造成事件嵌套:
// 主CSS文件(main.css)
@import “reset.css”;
@import “structure.css”;
@import “tutorials.css”;
@import “contact.css”;// 然后在tutorials.css文件中,会继续有@import
@import “document.css”;
@import “syntax-highlighter.css”;
我们这样写CSS文件,在文件中多了两个多余链接,因此会使页面加载变慢。SASS可以读取@import语句,链接CSS内容到一个文件中,减少了多余的请求,控制了CSS文件的大小。