性能优化 - 多方面性能优化
性能优化
CSS性能优化
内联首屏关键CSS
减少页面的首要内容出现在屏幕上的时间
大家应该都习惯于通过link标签引用外部CSS文件。但需要知道的是,将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前
,因为在HTML下载完成之后就能渲染了。
异步加载CSS
JavaScript动态创建样式表link元素
// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );
将link元素的media属性设置为用户浏览器不匹配的媒体类型
// 在文件加载完成之后,将media的值设为screen或all,从而让浏览器开始解析CSS。
<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
// 通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel改回去。
<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
上述的三种方法都较为古老。现在,rel="preload"5这一Web标准指出了如何异步加载资源,包括CSS类资源。
<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">
注意,as是必须的。忽略as属性,或者错误的as属性会使preload等同于XHR请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。as的可选值可以参考上述标准文档。
看起来,rel="preload"的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。
但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media方法能够更早地开始加载CSS
。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。
文件压缩
去除无用CSS
有选择地使用选择器
- 保持简单,不要使用嵌套过多过于复杂的选择器。
- 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。
- 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。
- 不要为了追求速度而放弃可读性与可维护性。
减少使用昂贵的属性
尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。
优化重排与重绘
减少重排
避免频繁触发重排操作
- 改变font-size和font-family
- 改变元素的内外边距
- 通过JS改变CSS类
- 通过JS获取DOM元素的位置相关属性(如width/height/left等)
- CSS伪类激活
- 滚动滚动条或者改变窗口大小
避免不必要的重绘
不要使用@import
使用link标签就行了。
原因:
首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。
其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载。
所以不要使用这一方法,使用link标签就行了。
加载技术
脚本加载的优化
通常有三种解决方案:将脚本放在HTML末尾、动态加载脚本以及异步加载脚本。
动态加载
所谓动态加载脚本就是利用javascript代码来加载脚本,通常是手工创建script元素,然后等到HTML文档解析完毕后插入到文档中去。这样就可以很好地控制脚本加载的时机,从而避免阻塞问题。
function loadJS(src) {
const script = document.createElement('script');
script.src = src;
document.getElementsByTagName('head')[0].appendChild(script);
}
loadJS('http://example.com/scq000.js');
异步加载
利用脚本的async和defer属性就可以实现这种需求:
<script type="text/javascript" src="./a.js" async></script>
<script type="text/javascript" src="./b.js" defer></script>
虽然利用了这两个属性的script标签都可以实现异步加载,同时不阻塞脚本解析。但是使用async属性的脚本执行顺序是不能得到保证的。而使用defer属性的脚本执行顺序可以得到保证。另一方面,defer属性是在html文档解析完成后,DOMContentLoaded事件之前就会执行js。async一旦加载完js后就会马上执行,最迟不超过window.onload事件。所以,如果脚本没有操作DOM等元素,或者与DOM时候加载完成无关,直接使用async脚本就好。如果需要DOM,就只能使用defer了。
解决异步加载脚本的问题
// 执行脚本
function exec(src) {
const script = document.createElement('script');
script.src = src;
// 返回一个独立的promise
return new Promise((resolve, reject) => {
var done = false;
script.onload = script.onreadystatechange = () => {
if (!done && (!script.readyState || script.readyState === "loaded" || script.readyState === "complete")) {
done = true;
// 避免内存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
}
script.onerror = reject;
document.getElementsByTagName('head')[0].appendChild(script);
});
}
function asyncLoadJS(dependencies) {
return Promise.all(dependencies.map(exec));
}
asyncLoadJS(['https://code.jquery.com/jquery-2.2.1.js', 'https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js']).then(() => console.log('all done'));
懒加载(lazyload)
虚拟代理加载
所谓虚拟代理加载,即为真正加载的对象事先提供一个代理或者说占位符。最常见的场景是在图片的懒加载中,先用一种loading的图片占位,然后再用异步的方式加载图片。等真正图片加载完成后就填充进图片节点中去。
// 页面中的图片url事先先存在其data-src属性上
const lazyLoadImg = function() {
const images = document.getElementsByTagName('img');
for(let i = 0; i < images.length; i++) {
if(images[i].getAttribute('data-src')) {
images[i].setAttribute('src', images[i].getAttribute('data-src'));
images[i].onload = () => images[i].removeAttribute('data-src');
}
}
}
惰性初始化
利用webpack实现脚本加载优化
import()方法
我们知道,在原生es6的语法中,提供了import和export的方式来管理模块。而其import关键字是被设置成静态的,因此不支持动态绑定。不过在es6的stage 3规范中,引入了一个新的方法import()使得动态加载模块成为可能。所以,你可以在项目中使用这样的代码:
$('#button').click(function() {
import('./dialog.js')
.then(dialog => {
//do something
})
.catch(err => {
console.log('模块加载错误');
});
});
//或者更优雅的写法
$('#button').click(async function() {
const dialog = await import('./dialog.js');
//do something with dialog
});
由于该语法是基于promise的,所以如果需要兼容旧浏览器,请确保在项目中使用es6-promise或者promise-polyfill。同时,如果使用的是babel,需要添加syntax-dynamic-import插件。
require.ensure
预加载
preload规范
preload 是w3c新出的一个标准。利用link的rel属性来声明相关“proload",从而实现预加载的目的。就像这样:
<link rel="preload" href="example.js" as="script">
其中rel属性是用来告知浏览器启用preload功能,而as属性是用来明确需要预加载资源的类型,这个资源类型不仅仅包括js脚本(script),还可以是图片(image),css(style),视频(media)等等。浏览器检测到这个属性后,就会预先加载资源。
这个规范目前兼容性方面还不是很好,所以可以先稍微了解一下。webpack现在也已经有相关的插件,如果感兴趣的话,请移步preload-webpack-plugin。
DNS Prefetch 预解析
还有一个可以优化网页速度的方式是利用dns的预解析技术。同preload类似,DNS Prefetch在网络层面上优化了资源加载的速度。我们知道,针对DNS的前端优化,主要分为减少DNS的请求次数,还有就是进行DNS预先获取。DNS prefetch就是为了实现这后者。其用法也很简单,只要在link标签上加上对应的属性就行了。
<meta http-equiv="x-dns-prefetch-control" content="on" /> /* 这是用来告知浏览器当前页面要做DNS预解析 */
<link rel="dns-prefetch" href="//example.com">
在支持该标准的浏览器上,会自动对链接中的地址域名做DNS解析缓存。不过,像Goolge、火狐这样的现代浏览器即使不设置这个属性,也能在后台做自动预解析。如果你的页面中需要大量访问不同域名的资源,可以利用这项技术加快资源的获取,从而获得更好的用户体验。需要注意的是,DNS预解析虽好,但是也不能滥用。如果对多页面重复DNS预解析,会增加DNS的查询次数。
作者:scq000
链接:https://juejin.im/post/59b73ef75188253db70acdb5
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。