Web前端性能优化进阶——完结篇

前言javascript

在以前的文章 如何优化网站性能,提升页面加载速度 中,咱们简单介绍了网站性能优化的重要性以及几种网站性能优化的方法(没有看过的能够狂戳 连接 移步过去看一下),那么今天咱们深刻讨论如何进一步优化网站性能。css

 

1、拆分初始化负载html

拆分初始化负载——听名字以为高大上,其实否则,土一点将讲就是将页面加载时须要的一堆JavaScript文件,分红两部分:渲染页面所必需的(页面出来,没他不行)和剩下的。页面初始化时,只加载必须的,其他的等会加载。前端

其实在现实生产环境中,对于大部分网站:页面加载完毕(window.onload触发)时,已经执行的JavaScript函数只占到所有加载量的少部分,譬如10%到20%或者更少。
java

注意:这里所说的页面加载完毕是指window.onload触发。window.onload何时出发?当页面中的内容(包括图片、样式、脚本)所有加载到浏览器时,才会触发window.onload,请与jQuery中$(document).ready做区分。jquery

上面咱们能够看到大部分JavaScript函数下载以后并未执行,这就形成了浪费。所以,若是咱们可以使用某种方式来延迟这部分未使用的代码的加载,那想必能够极大的缩减页面初始化时候的下载量。跨域

拆分文件  浏览器

咱们能够将原来的代码文件拆分红两部分:渲染页面所必需的(页面出来,没他不行)和剩下的;页面加载时只加载必须的,剩余的JavaScript代码在页面加载完成以后采用无阻塞下载技术当即下载。安全

须要注意的问题:性能优化

1. 咱们能够经过某些工具(譬如:Firebug)来得到页面加载时执行的函数,从而将这些代码拆分红一个单独的文件。那么问题来了,有些代码在页面加载的时候不会执行,可是确实必须的,譬如条件判断代码或者错误处理的代码。另外JavaScript的做用域问题是相对比较奇葩的,这些都给拆分形成了很大的困难

2. 关于未定义标识符的错误,譬如已加载的JavaScript代码在执行时,引用了一个被咱们拆分延迟加载的JavaScript代码中的变量,就会形成错误。举个栗子:

页面加载完成时用户点击了某个按钮(此时原JavaScript文件被拆分,只下载了页面加载所必需的的代码),而监听此按钮的代码尚未被下载(由于这不是页面加载所必需的,因此在拆分时被降级了),因此点击就没有响应或者直接报错(找不到事件处理函数)。

解决方案:

1. 在低优先级的代码被加载完成时,按钮处于不可用状态(可附带提示信息);

2. 使用桩函数,桩函数与原函数名字相同,可是函数体为空,这样就能够防止报错了。当剩余的代码加载完成时,桩函数就被原来的同名函数覆盖掉。咱们能够作的再狠一点:记录用户的行为(点击、下拉),当剩余的代码加载完成时,再根据记录调用相应的函数。

 

2、无阻塞加载脚本

大多数浏览器能够并行下载页面所须要的组件,然而对于脚本文件却并不是如此。脚本文件在下载时,在其下载完成、解析执行完毕以前,并不会下载任何其余的内容。这么作是有道理的,由于浏览器并不知道脚本是否会操做页面的内容;其次,后面加载的脚本可能会依赖前面的脚本 ,若是并行下载,后面的脚本可能会先下载完并执行,产生错误。因此,以前咱们讲到了脚本应该尽量放在底部接近</body>的位置,就是为了尽可能减小整个页面的影响。  

接下来咱们讨论几种技术可使页面不会被脚本的下载阻塞:

一、Script Defer

<script type="text/javascript" src="file1.js" defer></script>

支持浏览器: IE4+ 、Firefox 3.5+以及其它新版本的浏览器

defer表示该脚本不打算修改DOM,能够稍后执行。

二、动态脚本元素

var script = document.createElement ("script");
script.type = "text/javascript";
script.src = "a.js"; 
document.body.appendChild(script);

用动态建立script标签的方法不会阻塞其它的页面处理过程,在IE下还能够并行下载脚本。

三、XHR(XMLHttpRequest)Eval

该方法经过XMLHttpRequest以非阻塞的方式从服务端加载脚本,加载完成以后经过eval解析执行。

 1 var xhr = getXHRObj();
 2 
 3 xhr.onreadystatechange = function() {
 4     if(xhr.readyState == 4 && xhr.status == 200) {
 5         eval(xhr.responseText);
 6     }
 7 };
 8 
 9 xhr.open('GET','text.js',true);
10 xhr.send('');
11 
12 function getXHRObj() {
13     // ......
14     return xhrObj;
15 }

该方式不会阻塞页面中其它组件的下载。

缺点:(1)脚本的域必须和主页面在相同的域中;(2)eval的安全性问题

四、XHR Injection              

 XMLHttpRequest Injection(XHR脚本注入)和XHR Eval相似,都是经过 XMLHttpRequest 来获取JavaScript的。 在得到文件以后  ,将会建立一个script标签将获得的代码注入页面。

 1 var xhr = new XMLHttpRequest(); 
 2 xhr.open("GET", "test.js", true); 
 3 xhr.send('');
 4 xhr.onreadystatechange = function(){
 5     if (xhr.readyState == 4){
 6        if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ 
 7             var script = document.createElement("script"); 
 8             script.type = "text/javascript";
 9             script.text = xhr.responseText;
10             document.body.appendChild(script);
11       } 
12    }
13 }; 

 XMLHttpRequest获取的内容必须和主页处于相同的域。 

五、Script元素的src属性

1 var script = document.createElement('script');
2 script.src = 'http://a.com/a.js'
3 document.body.appendChild(script);

这种方式不会阻塞其它组件,并且容许跨域获取脚本。

六、IFrame嵌入Script 

页面中的iframe和其它元素是并行下载的,所以能够利用这点将须要加载的脚本嵌入iframe中。

 <iframe src="1.html" frameborder="0" width=0 height="0"></iframe>

注意:这里是1.html而不是1.js,iframe觉得这是html文件,而咱们则把要加载的脚本嵌入其中。

这种方式要求iframe的请求url和主页面同域。

 

3、整合异步脚本

上面咱们介绍了如何异步加载脚本,提升页面的加载速度。可是异步加载脚本也是存在问题的,譬如行内脚本依赖外部脚本里面定义的标识,这样当内联的脚本执行的时候外部脚本尚未加载完成,那么就会发生错误。

那么接下来咱们就讨论一下如何实如今异步加载脚本的时候又能保证脚本的可以按照正确的顺序执行。

单个外部脚本与内联脚本

譬如:内联脚本使用了外部脚本定义的标识符,外部脚本采用异步加载提升加载速度

$(".button").click(function() {
    alert("hello");
});         
<script src="jquery.js"></script>

一、Script Onload

经过Script的onload方法监听脚本是否加载完成,将依赖外部文件的内联代码写在init函数中,在onload事件函数中调用init函数。

script.onload的支持状况:

IE六、IE七、IE8不支持onload,能够用onreadystatechange来代替。

IE九、IE10先触发onload事件,再触发onreadystatechange事件

IE11(Edge)只触发onload事件

其余浏览器支持均支持onload,在opera中onload和onreadystatechange均有效。

 1 function init() {
 2     // inline code......
 3 }
 4 var script = document.createElement("script");  
 5 script.type = "text/javascript";  
 6 script.src = "a.js";
 7 script.onloadDone = false;    
 8 
 9 script.onreadystatechange = function(){  
10      if((script.readyState == 'loaded' || script.readyState == 'complete') && !script.onloadDone){  
11         // alert("onreadystatechange");  
12         init();
13       }
14 }
15 
16 script.onload = function(){   
17     // alert("onload");
18     init();
19     script.onloadDone = true;
20 }    
21 
22 document.getElementsByTagName('head')[0].appendChild(script);    

这里onloadDone用来防止在IE九、IE10已结opera中初始化函数执行两次。

Script Onload是整合内联脚本和外部异步加载脚本的首选。

推荐指数:5颗星

二、硬编码回调

将依赖外部文件的内联代码写在init函数中,修改异步加载的文件,在文件中添加对init函数的调用。

缺点:要修改外部文件,而咱们通常不会修改第三方的插件;缺少灵活性,改变回调接口时,须要修改外部的脚本。

推荐指数:2颗星

三、定时器

将依赖外部文件的内联代码写在init函数中,采用定时器的方法检查依赖的名字空间是否存在。若已经存在,则调用init函数;若不存在,则等待一段时间在检查。

function init() {
    // inline code......
}
var script = document.createElement("script");  
script.type = "text/javascript";  
script.src = "jquery.js";
document.getElementsByTagName('head')[0].appendChild(script);    

function timer() {
    if("undefined" === typeof(jQuery)) {
        setTimeout(timer,500);
    }
    else {
        init();
    }
}

timer();

缺点:

若是setTimeout设置的时间间隔太小,则可能会增长页面的开销;若是时间间隔过大,就会发生外部脚本加载完毕而行内脚本须要间隔一段才能时间执行的情况,从而形成浪费。

若是外部脚本(jquery.js)加载失败,则这个轮询将会一直持续下去。

增长维护成本,由于咱们须要经过外部脚本的特定标识符来判断脚本是否加载完毕,若是外部脚本的标识符变了,则行内的代码也须要改变。

推荐指数:2颗星

四、window.onload

咱们可使用window.onload事件来触发行内代码的执行,可是这要求外部的脚本必须在window.onload事件触发以前下载完毕。

在 无阻塞加载脚本提到的技术中,IFrame嵌入Script 、动态脚本元素 、Script Defer 能够知足这点要求。

1 function init() {
2     // inline code......
3 }
4 if(window.addEventListener) {
5     window.addEventListener("load",init,false);
6 }
7 else if(window.attachEvent) {
8     window.attachEvent("onload",init);
9 }

缺点:这会阻塞window.onload事件,因此并非一个很好的办法;若是页面中还有不少其余资源(譬如图片、Flash等),那么行内脚本将会延迟执行(就算它依赖的外部脚本一早就加载完了),由于window.onload不会触发。

推荐指数:3颗星

五、降级使用script

来来来,先看看它什么样子:

<script src="jquery.js" type="text/javascript">
    $(".button").click(function() {
        alert("hello");
    });
</script>

然并卵,目前尚未浏览器能够实现这种方式,通常状况下,外部脚本(jquery.js)加载成功后,两个标签之间的代码就不会执行了。

可是咱们能够改进一下:修改外部脚本的代码,让它在DOM树种搜索本身,用innerHTML获取本身内部的代码,而后用eval执行,就能够解决问题了。

而后咱们在修改一下让它异步加载,就变成了这样:

1 function init() {
2     // inline code......
3 }
4 var script = document.createElement("script");  
5 script.type = "text/javascript";  
6 script.src = "jquery.js";
7 script.innerHTML = "init()'"
8 document.getElementsByTagName('head')[0].appendChild(script); 

而在外部脚本中咱们须要添加以下代码:

1 var scripts = document.getElementsByTagName("script");
2 
3 for(var i = 0; i < scripts.length;i++) {
4     if(-1 != scripts[i].src.indexOf('jquery.js')) {
5         eval(script.innerHTML);
6         break;
7     }
8 }

这样就大功告成 。然而,缺点也很明显,咱们仍是须要修改外部文件的代码。

推荐指数:2颗星

内联脚本、多个外部脚本相互依赖

举个栗子:

内联脚本依赖a.js,a.js依赖b.js;

这种状况比较麻烦(好吧,是由于我太菜),简单介绍一下思路:

确保a.js在b.js以后执行,内联脚本在a.js以后执行。

咱们可使用XMLHttpRequest同时异步获取两个脚本,若是a.js先下载完成,则判断b.js是否下载完成,若是下载完成则执行,不然等待,a.js执行以后就能够调用内联脚本执行了。b.js下载完成以后便可执行。

代码大概这样(求指正):

 1 function init() {
 2     // inline code......
 3 }
 4 
 5 
 6 var xhrA = new XMLHttpRequest();
 7 var xhrB = new XMLHttpRequest();  
 8 var scriptA , scriptB;
 9 
10 var scriptA = document.createElement("script"); 
11 scriptA.type = "text/javascript";
12 
13 var scriptB = document.createElement("script"); 
14 scriptB.type = "text/javascript";
15 
16 scriptA = scriptB = false;
17 
18 xhrA.open("GET", "a.js", true); 
19 xhrA.send('');
20 xhrA.onreadystatechange = function(){
21     if (xhr.readyState == 4){
22         if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ 
23             scriptA.text = xhr.responseText;
24             scriptA = true;
25             if(scriptB) {
26                 document.body.appendChild(scriptA);
27                 init();
28             }    
29         } 
30     }
31 }; 
32 
33 xhrB.open("GET", "b.js", true); 
34 xhrB.send('');
35 xhrB.onreadystatechange = function(){
36     if (xhr.readyState == 4){
37         if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ 
38             scriptB.text = xhr.responseText;
39             scriptB = true
40             document.body.appendChild(scriptB);
41             if(scriptA) {
42                 document.body.appendChild(scriptA);
43                 init();
44             }    
45         } 
46     }
47 }; 

 

4、编写高效的JavaScript

以前讲过了,你们能够猛戳 这里 看一下。

 

5、CSS选择器优化

一、在谈论选择器优化以前,咱们先简单介绍一下选择器的类型:

ID选择器 : #id;

类选择器: .class

标签选择器: a

兄弟选择器:#id + a 

子选择器: #id > a

后代选择器: #id a

通赔选择器: *

属性选择器: input[type='input']

伪类和伪元素:a:hover , div:after

组合选择器:#id,.class

二、浏览器的匹配规则

#abc > a怎么匹配?  有人可能会觉得:先找到id为abc的元素,再查找子元素为a的元素!!too young,too simple!

其实,浏览器时从右向左匹配选择符的!!!那么上面的写法效率就低了:先查找页面中的全部a标签,在看它的父元素是否是id为abc

知道了浏览器的匹配规则咱们就能尽量的避免开销很大的选择器了:

避免通配规则

除了 * 以外,还包括子选择器、后台选择器等。

而它们之间的组合更加逆天,譬如:li *

浏览器会查找页面的全部元素,而后一层一层地寻找他的祖先,看是否是li,这对可能极大地损耗性能。

不限定ID选择器

ID就是惟一的,不要写成相似div#nav这样,不必。 

不限定class选择器

咱们能够进一步细化类名,譬如li.nav  写成 nav-item

尽可能避免后代选择器

一般后代选择器是开销最高的,若是能够,请使用子选择器代替。

替换子选择器

若是能够,用类选择器代替子选择器,譬如

nav > li 改为 .nav-item

依靠继承

了解那些属性能够依靠继承得来,从而避免重复设定规则。

三、关键选择符

选择器中最右边的选择符成为关键选择符,它对浏览器执行的工做量起主要影响。

举个栗子:

div div li span.class-special

乍一看,各类后代选择器组合,性能确定不能忍。其实仔细一想,浏览器从右向左匹配,若是页面中span.class-special的元素只有一个的话,那影响并不大啊。

反过来看,若是是这样

span.class-special li div div ,尽管span.class-special不多,可是浏览器从右边匹配,查找页面中全部div在层层向上查找,那性能天然就低了。

四、重绘与回流

优化css选择器不只仅提升页面加载时候的效率,在页面回流、重绘的时候也能够获得不错的效果,那么接下来咱们说一下重绘与回流。

4.一、从浏览器的渲染过程谈起

解析HTML构建dom树→构建render树→布局render树→绘制render树

1)构建dom树

根据得到的html代码生成一个DOM树,每一个节点表明一个HTML标签,根节点是document对象。dom树种包含了全部的HTML标签,包括未显示的标签(display:none)和js添加的标签。

2)构建cssom树

将获得全部样式(浏览器和用户定义的css)除去不能识别的(错误的以及css hack),构建成一个cssom树

3)cssom和dom结合生成渲染树,渲染树中不包括隐藏的节点包括(display:none、head标签),并且每一个节点都有本身的style属性,渲染树种每个节点成为一个盒子(box)。注意:透明度为100%的元素以及visibility:hidden的元素也包含在渲染树之中,由于他们会影响布局。

4)浏览器根据渲染树来绘制页面

4.二、重绘(repaint)与回流(reflow)

1)重绘   当渲染树中的一部分或者所有由于页面中某些元素的布局、显示与隐藏、尺寸等改变须要从新构建,这就是回流。每一个页面至少会发生一次回流,在页面第一次加载的时候发生。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并从新构造这部分渲染树,完成回流后,浏览器会从新绘制受影响的部分到屏幕中,该过程成为重绘。

2. 当渲染树中的一些元素须要更新属性,而这些属性不会影响布局,只影响元素的外观、风格,好比color、background-color,则称为重绘。

注意:回流必将引发重绘,而重绘不必定会引发回流。

4.三、回流什么时候发生:

当页面布局和几何属性改变时就须要回流。下述状况会发生浏览器回流:

一、添加或者删除可见的DOM元素;

二、元素位置改变;

三、元素尺寸改变——边距、填充、边框、宽度和高度

四、内容改变——好比文本改变或者图片大小改变而引发的计算值宽度和高度改变;

五、页面渲染初始化;

六、浏览器窗口尺寸改变——resize事件发生时;

4.四、如何影响性能

页面上任何一个结点触发reflow,都会致使它的子结点及祖先结点从新渲染。

每次重绘和回流发生时,浏览器会根据对应的css从新绘制须要渲染的部分,若是你的选择器不优化,就会致使效率下降,因此优化选择器的重要性可见一斑。

 

6、尽可能少用iframe

在写网页的时候,咱们可能会用到iframe,iframe的好处是它彻底独立于父文档。iframe中包含的JavaScript文件访问其父文档是受限的。例如,来自不一样域的iframe不能访问其父文档的Cookie。

开销最高的DOM元素

一般建立iframe元素的开销要比建立其它元素的开销高几十倍甚至几百倍。

iframe阻塞onload事件

一般咱们会但愿window.onload事件可以尽量触发,缘由以下:

  • 咱们可能在onload事件处理函数中编写了用于初始化UI的代码;
  • onload事件触发时,浏览器中止“忙指示器”,并向用户反馈页面已经准备就绪。
  • 部分低版本浏览器(IE六、IE七、IE八、Safari三、Safari四、Chrome一、Chrome2等)只有onload事件触发以后才会触发unload事件。有时,咱们会把一些重要的操做和window的unload事件绑定在一块儿。例如,减小内存泄露的代码。若是onload花费时间太长,用户可能会离开页面,那么在这些浏览器中unload可能就永远不会执行了。

一般状况下,iframe中的内容对页面来讲不是很重要的(譬如第三方的广告),咱们不该该由于这些内容而延迟window.onload事件的触发。

综上,即便iframe是空的,其开销也会很高,并且他会阻塞onload事件。因此,咱们应该尽量避免iframe的使用。

 

7、图片优化

在大多数网站中,图片的大小每每能占到一半以上,因此优化图片能带来更好的效果;并且,对图片的优化,还能够实现再不删减网站功能的条件下实现网站性能的提高。

一、图像格式

GIF

透明:容许二进制类型的透明度,要么彻底透明,要么不透明。

动画:支持动画。动画由若干帧组成。

无损:GIF是无损的

逐行扫描:生成GIF时,会使用压缩来减少文件大小。压缩时,逐行扫描像素,当图像在水平方向有不少重复颜色时,能够得到更好的压缩效果。

支持隔行扫描

GIF有256色限制,因此不适合显示照片。能够用来显示图形,可是PNG8是用来显示图形的最佳方式。因此,通常在须要动画时才用到GIF。

JPEG

有损

不支持动画和透明

支持隔行扫描

PNG

透明:PNG支持彻底的alpha透明

动画:目前无跨浏览器解决方案

无损

逐行扫描:和GIF相似,对水平方向有重复颜色的图像压缩比高。

支持隔行扫描

隔行扫描是什么:

网速很慢时,部分图像支持对那些连续采样的图像进行隔行扫描。隔行扫描可让用户在完整下载图像以前,能够先看到图像的一个粗略的版本,从而消除页面被延迟加载的感受。

二、PNG在IE6中的奇怪现象

全部在调色板PNG中的半透明像素在IE6下会显示为完整的透明。

真彩色PNG中的alpha透明像素,会显示为背景色

三、无损图像优化

PNG图像优化

PNG格式图像信息保存在”块“中,对于Web现实来讲,大部分块并不是必要,咱们能够将其删除。

推荐工具:Pngcrush

JPEG图像优化

剥离元数据(注释、其余内部信息等)

这些元数据能够安全删除不会影响图片质量。

推荐工具jpegtran

GIF转换成PNG

前面提到GIF的功能吃了动画以外,彻底能够用PNG8来代替,因此咱们使用PNG代替GIF

推荐工具ImageMagick

优化GIF动画

由于动画里面有不少帧,而且部份内容在不少帧上都是同样的,因此咱们能够将图像里面连续帧中的重复像素移除。

推荐工具:Gifsicle

四、CSS sprite优化

若是网站页面较少,能够将图像放在一个超级CSS sprite中

看看Google就使用了一个:

最佳实践:

  • 按照颜色合并:颜色相近的突变组合在一块儿
  • 避免没必要要的空白
  • 元素水平排列:比竖直排列稍微小点
  • 将颜色限制在25种以内(尽可能)
  • 先优化单独的图像,再优化Sprite
  • 经过控制大小和对齐减小反锯齿的数量。
  • 避免使用对角线渐变,这种渐变没法被平铺。
  • IE6中alpha透明图像单独使用sprite
  • 每2-3个像素改变渐变颜色,而不是每一个
  • 避免对图像缩放
  • 若是咱们须要一张小的图像,就不必在下载一张大的图像以后在HTML中将其缩小。
  • 譬如咱们须要一个100*100的图像,咱们能够如今服务器端改变图像的大小,这样能够节省下载的流量。

五、避免对图像缩放

若是咱们在页面中用不到大的图像,就不必下载一个很大的而后用css限制他的大小。

譬如咱们须要一个100*100的图像,咱们能够如今服务器端改变图像的大小,这样能够节省下载的流量。

 

8、划分主域

在以前咱们谈到为了减小DNS的查找,咱们应该减小域的数量。但有的时候增长域的数量反而会提升性能,关键是找到提高性能的关键路径。若是一个域提供了太多的资源而成为关键路径,那么将资源分配到多个域上(咱们成为域划分),可使页面加载更快。

当单个域下载资源成为瓶颈时,可将资源分配到多个域上。经过并行的下载数来提升页面速度。

譬如YouTube序列化域名:i1.ytimg.com、i2.ytimg.com、i3.ytimg.com、i4.ytimg.com

IP地址和主机名

浏览器执行“每一个服务端最大链接数”的限制是根据URL上的主机名,而不是解析出来的IP地址。所以,咱们能够没必要额外部署服务器,而是为新域创建一条CNAME记录。CNAME仅仅是域名的别名,即便域名都指向同一个服务器,浏览器依旧会为每一个主机名开放最大链接数。

譬如,咱们为www.abc.com创建一个别名abc.com,这两个主机名有相同的IP地址,浏览器会将每一个主机名当作一个单独的服务端。

另外,研究代表,域的数量从一个增长到两个性能会获得提升,但超过两个时就可能出现负面影响了。最终数量取决于资源的大小和数量,但分为两个域是很好的经验。

 

若是是原创文章,转载注明出处http://www.cnblogs.com/MarcoHan/

 

以前讲了两篇关于Web性能优化的文章,Web前端性能优化——编写高效的JavaScript 和Web前端性能优化——如何提升页面加载速度。那么关于Web性能优化,就暂且说到这里了,若是有点用的话,不点一下推荐吗?

相关文章
相关标签/搜索