前言
在之前的文章 如何优化网站性能, 提高页面加载速度 中, 我们简单介绍了网站性能优化的重要性以及几种网站性能优化的方法(没有看过的可以狂戳 链接 移步过去看一下), 那么今天我们深入讨论如何进一步优化网站性能.
一, 拆分初始化负载
拆分初始化负载 -- 听名字觉得高大上, 其实不然, 土一点将讲就是将页面加载时需要的一堆 JavaScript 文件, 分成两部分: 渲染页面所必需的 (页面出来, 没他不行) 和剩下的. 页面初始化时, 只加载必须的, 其余的等会加载.
其实在现实生产环境中, 对于大部分网站: 页面加载完毕 (window.onload 触发) 时, 已经执行的 JavaScript 函数只占到全部加载量的少部分, 譬如 10% 到 20% 或者更少.
注意: 这里所说的页面加载完毕是指 window.onload 触发. window.onload 什么时候出发? 当页面中的内容 (包括图片, 样式, 脚本) 全部加载到浏览器时, 才会触发 window.onload, 请与 jQuery 中 $(document).ready 作区分.
上面我们可以看到大部分 JavaScript 函数下载之后并未执行, 这就造成了浪费. 因此, 如果我们能够使用某种方式来延迟这部分未使用的代码的加载, 那想必可以极大的缩减页面初始化时候的下载量.
拆分文件
我们可以将原来的代码文件拆分成两部分: 渲染页面所必需的 (页面出来, 没他不行) 和剩下的; 页面加载时只加载必须的, 剩余的 JavaScript 代码在页面加载完成之后采用无阻塞下载技术立即下载.
需要注意的问题:
1. 我们可以通过某些工具 (譬如: Firebug) 来获得页面加载时执行的函数, 从而将这些代码拆分成一个单独的文件. 那么问题来了, 有些代码在页面加载的时候不会执行, 但是确实必须的, 譬如条件判断代码或者错误处理的代码. 另外 JavaScript 的作用域问题是相对比较奇葩的, 这些都给拆分造成了很大的困难
2. 关于未定义标识符的错误, 譬如已加载的 JavaScript 代码在执行时, 引用了一个被我们拆分延迟加载的 JavaScript 代码中的变量, 就会造成错误. 举个栗子:
页面加载完成时用户点击了某个按钮(此时原 JavaScript 文件被拆分, 只下载了页面加载所必需的的代码), 而监听此按钮的代码还没有被下载(因为这不是页面加载所必需的, 所以在拆分时被降级了), 所以点击就没有响应或者直接报错(找不到事件处理函数).
解决方案:
1. 在低优先级的代码被加载完成时, 按钮处于不可用状态(可附带提示信息);
2. 使用桩函数, 桩函数与原函数名字相同, 但是函数体为空, 这样就可以防止报错了. 当剩余的代码加载完成时, 桩函数就被原来的同名函数覆盖掉. 我们可以做的再狠一点: 记录用户的行为(点击, 下拉), 当剩余的代码加载完成时, 再根据记录调用相应的函数.
二, 无阻塞加载脚本
大多数浏览器可以并行下载页面所需要的组件, 然而对于脚本文件却并非如此. 脚本文件在下载时, 在其下载完成, 解析执行完毕之前, 并不会下载任何其他的内容. 这么做是有道理的, 因为浏览器并不知道脚本是否会操作页面的内容; 其次, 后面加载的脚本可能会依赖前面的脚本 , 如果并行下载, 后面的脚本可能会先下载完并执行, 产生错误. 所以, 之前我们讲到了脚本应该尽可能放在底部接近</body > 的位置, 就是为了尽量减少整个页面的影响.
接下来我们讨论几种技术可以使页面不会被脚本的下载阻塞:
- 1,Script Defer
- <script type="text/javascript" src="file1.js" defer></script>
支持浏览器: IE4+ ,Firefox 3.5 + 以及其它新版本的浏览器
defer 表示该脚本不打算修改 DOM, 可以稍后执行.
2, 动态脚本元素
- var script = document.createElement ("script");
- script.type = "text/javascript";
- script.src = "a.js";
- document.body.appendChild(script);
用动态创建 script 标签的方法不会阻塞其它的页面处理过程, 在 IE 下还可以并行下载脚本.
3,XHR(XMLHttpRequest)Eval
该方法通过 XMLHttpRequest 以非阻塞的方式从服务端加载脚本, 加载完成之后通过 eval 解析执行.
- var xhr = getXHRObj();
- xhr.onreadystatechange = function() {
- if(xhr.readyState == 4 && xhr.status == 200) {
- eval(xhr.responseText);
- }
- };
- xhr.open('GET','text.js',true);
- xhr.send('');
- function getXHRObj() {
- // ......
- return xhrObj;
- }
该方式不会阻塞页面中其它组件的下载.
缺点:(1)脚本的域必须和主页面在相同的域中;(2)eval 的安全性问题
4,XHR Injection
XMLHttpRequest Injection(XHR 脚本注入)和 XHR Eval 类似, 都是通过 XMLHttpRequest 来获取 JavaScript 的. 在获得文件之后 , 将会创建一个 script 标签将得到的代码注入页面.
- var xhr = new XMLHttpRequest();
- xhr.open("GET", "test.js", true);
- xhr.send('');
- xhr.onreadystatechange = function(){
- if (xhr.readyState == 4){
- if (xhr.status>= 200 && xhr.status <300 || xhr.status == 304){
- var script = document.createElement("script");
- script.type = "text/javascript";
- script.text = xhr.responseText;
- document.body.appendChild(script);
- }
- }
- };
XMLHttpRequest 获取的内容必须和主页处于相同的域.
5,Script 元素的 src 属性
- var script = document.createElement('script');
- script.src = 'http://a.com/a.js'
- document.body.appendChild(script);
这种方式不会阻塞其它组件, 而且允许跨域获取脚本.
6,IFrame 嵌入 Script
页面中的 iframe 和其它元素是并行下载的, 因此可以利用这点将需要加载的脚本嵌入 iframe 中.
<iframe src="1.html" frameborder="0" width=0 height="0"></iframe>
注意: 这里是 1.html 而不是 1.js,iframe 以为这是 html 文件, 而我们则把要加载的脚本嵌入其中.
这种方式要求 iframe 的请求 url 和主页面同域.
三, 整合异步脚本
上面我们介绍了如何异步加载脚本, 提高页面的加载速度. 但是异步加载脚本也是存在问题的, 譬如行内脚本依赖外部脚本里面定义的标识, 这样当内联的脚本执行的时候外部脚本还没有加载完成, 那么就会发生错误.
那么接下来我们就讨论一下如何实现在异步加载脚本的时候又能保证脚本的能够按照正确的顺序执行.
单个外部脚本与内联脚本
譬如: 内联脚本使用了外部脚本定义的标识符, 外部脚本采用异步加载提高加载速度
- $(".button").click(function() {
- alert("hello");
- });
- <script src="jquery.js"></script>
- 1,Script Onload
通过 Script 的 onload 方法监听脚本是否加载完成, 将依赖外部文件的内联代码写在 init 函数中, 在 onload 事件函数中调用 init 函数.
script.onload 的支持情况:
IE6,IE7,IE8 不支持 onload, 可以用 onreadystatechange 来代替.
IE9,IE10 先触发 onload 事件, 再触发 onreadystatechange 事件
IE11(Edge)只触发 onload 事件
其他浏览器支持均支持 onload, 在 opera 中 onload 和 onreadystatechange 均有效.
- function init() {
- // inline code......
- }
- var script = document.createElement("script");
- script.type = "text/javascript";
- script.src = "a.js";
- script.onloadDone = false;
- script.onreadystatechange = function(){
- if((script.readyState == 'loaded' || script.readyState == 'complete') && !script.onloadDone){
- // alert("onreadystatechange");
- init();
- }
- }
- script.onload = function(){
- // alert("onload");
- init();
- script.onloadDone = true;
- }
- document.getElementsByTagName('head')[0].appendChild(script);
这里 onloadDone 用来防止在 IE9,IE10 已结 opera 中初始化函数执行两次.
Script Onload 是整合内联脚本和外部异步加载脚本的首选.
推荐指数: 5 颗星
2, 硬编码回调
将依赖外部文件的内联代码写在 init 函数中, 修改异步加载的文件, 在文件中添加对 init 函数的调用.
缺点: 要修改外部文件, 而我们一般不会修改第三方的插件; 缺乏灵活性, 改变回调接口时, 需要修改外部的脚本.
推荐指数: 2 颗星
3, 定时器
将依赖外部文件的内联代码写在 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 颗星
4,window.onload
我们可以使用 window.onload 事件来触发行内代码的执行, 但是这要求外部的脚本必须在 window.onload 事件触发之前下载完毕.
在 无阻塞加载脚本提到的技术中, IFrame 嵌入 Script , 动态脚本元素 ,Script Defer 可以满足这点要求.
- function init() {
- // inline code......
- }
- if(window.addEventListener) {
- window.addEventListener("load",init,false);
- }
- else if(window.attachEvent) {
- window.attachEvent("onload",init);
- }
缺点: 这会阻塞 window.onload 事件, 所以并不是一个很好的办法; 如果页面中还有很多其他资源(譬如图片, Flash 等), 那么行内脚本将会延迟执行(就算它依赖的外部脚本一早就加载完了), 因为 window.onload 不会触发.
推荐指数: 3 颗星
5, 降级使用 script
来来来, 先看看它什么样子:
- <script src="jquery.js" type="text/javascript">
- $(".button").click(function() {
- alert("hello");
- });
- </script>
然并卵, 目前还没有浏览器可以实现这种方式, 一般情况下, 外部脚本 (jquery.js) 加载成功后, 两个标签之间的代码就不会执行了.
但是我们可以改进一下: 修改外部脚本的代码, 让它在 DOM 树种搜索自己, 用 innerHTML 获取自己内部的代码, 然后用 eval 执行, 就可以解决问题了.
然后我们在修改一下让它异步加载, 就变成了这样:
- function init() {
- // inline code......
- }
- var script = document.createElement("script");
- script.type = "text/javascript";
- script.src = "jquery.js";
- script.innerHTML = "init()'"
- document.getElementsByTagName('head')[0].appendChild(script);
而在外部脚本中我们需要添加如下代码:
- var scripts = document.getElementsByTagName("script");
- for(var i = 0; i <scripts.length;i++) {
- if(-1 != scripts[i].src.indexOf('jquery.js')) {
- eval(script.innerHTML);
- break;
- }
- }
这样就大功告成 . 然而, 缺点也很明显, 我们还是需要修改外部文件的代码.
推荐指数: 2 颗星
内联脚本, 多个外部脚本相互依赖
举个栗子:
内联脚本依赖 a.js,a.js 依赖 b.js;
这种情况比较麻烦(好吧, 是因为我太菜), 简单介绍一下思路:
确保 a.js 在 b.js 之后执行, 内联脚本在 a.js 之后执行.
我们可以使用 XMLHttpRequest 同时异步获取两个脚本, 如果 a.js 先下载完成, 则判断 b.js 是否下载完成, 如果下载完成则执行, 否则等待, a.js 执行之后就可以调用内联脚本执行了. b.js 下载完成之后即可执行.
代码大概这样(求指正):
- function init() {
- // inline code......
- }
- var xhrA = new XMLHttpRequest();
- var xhrB = new XMLHttpRequest();
- var scriptA , scriptB;
- var scriptA = document.createElement("script");
- scriptA.type = "text/javascript";
- var scriptB = document.createElement("script");
- scriptB.type = "text/javascript";
- scriptA = scriptB = false;
- xhrA.open("GET", "a.js", true);
- xhrA.send('');
- xhrA.onreadystatechange = function(){
- if (xhr.readyState == 4){
- if (xhr.status>= 200 && xhr.status <300 || xhr.status == 304){
- scriptA.text = xhr.responseText;
- scriptA = true;
- if(scriptB) {
- document.body.appendChild(scriptA);
- init();
- }
- }
- }
- };
- xhrB.open("GET", "b.js", true);
- xhrB.send('');
- xhrB.onreadystatechange = function(){
- if (xhr.readyState == 4){
- if (xhr.status>= 200 && xhr.status <300 || xhr.status == 304){
- scriptB.text = xhr.responseText;
- scriptB = true
- document.body.appendChild(scriptB);
- if(scriptA) {
- document.body.appendChild(scriptA);
- init();
- }
- }
- }
- };
四, 编写高效的 JavaScript
之前讲过了, 大家可以猛戳 这里 看一下.
五, CSS 选择器优化
1, 在谈论选择器优化之前, 我们先简单介绍一下选择器的类型:
ID 选择器 : #id;
类选择器: .class
标签选择器: a
兄弟选择器:#id + a
子选择器: #id> a
后代选择器: #id a
通赔选择器: *
属性选择器: input[type='input']
伪类和伪元素: a:hover , div:after
组合选择器:#id,.class
2, 浏览器的匹配规则
#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
依靠继承
了解那些属性可以依靠继承得来, 从而避免重复设定规则.
3, 关键选择符
选择器中最右边的选择符成为关键选择符, 它对浏览器执行的工作量起主要影响.
举个栗子:
div div li span.class-special
乍一看, 各种后代选择器组合, 性能肯定不能忍. 其实仔细一想, 浏览器从右向左匹配, 如果页面中 span.class-special 的元素只有一个的话, 那影响并不大啊.
反过来看, 如果是这样
span.class-special li div div , 尽管 span.class-special 很少, 但是浏览器从右边匹配, 查找页面中所有 div 在层层向上查找, 那性能自然就低了.
4, 重绘与回流
优化 css 选择器不仅仅提高页面加载时候的效率, 在页面回流, 重绘的时候也可以得到不错的效果, 那么接下来我们说一下重绘与回流.
4.1, 从浏览器的渲染过程谈起
解析 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.2, 重绘 (repaint) 与回流(reflow)
1)重绘当渲染树中的一部分或者全部因为页面中某些元素的布局, 显示与隐藏, 尺寸等改变需要重新构建, 这就是回流. 每个页面至少会发生一次回流, 在页面第一次加载的时候发生. 在回流的时候, 浏览器会使渲染树中受到影响的部分失效, 并重新构造这部分渲染树, 完成回流后, 浏览器会重新绘制受影响的部分到屏幕中, 该过程成为重绘.
2. 当渲染树中的一些元素需要更新属性, 而这些属性不会影响布局, 只影响元素的外观, 风格, 比如 color,background-color, 则称为重绘.
注意: 回流必将引起重绘, 而重绘不一定会引起回流.
4.3, 回流何时发生:
当页面布局和几何属性改变时就需要回流. 下述情况会发生浏览器回流:
1, 添加或者删除可见的 DOM 元素;
2, 元素位置改变;
3, 元素尺寸改变 -- 边距, 填充, 边框, 宽度和高度
4, 内容改变 -- 比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
5, 页面渲染初始化;
6, 浏览器窗口尺寸改变 --resize 事件发生时;
4.4, 如何影响性能
页面上任何一个结点触发 reflow, 都会导致它的子结点及祖先结点重新渲染.
每次重绘和回流发生时, 浏览器会根据对应的 css 重新绘制需要渲染的部分, 如果你的选择器不优化, 就会导致效率降低, 所以优化选择器的重要性可见一斑.
六, 尽量少用 iframe
在写网页的时候, 我们可能会用到 iframe,iframe 的好处是它完全独立于父文档. iframe 中包含的 JavaScript 文件访问其父文档是受限的. 例如, 来自不同域的 iframe 不能访问其父文档的 Cookie.
开销最高的 DOM 元素
通常创建 iframe 元素的开销要比创建其它元素的开销高几十倍甚至几百倍.
iframe 阻塞 onload 事件
通常我们会希望 window.onload 事件能够尽可能触发, 原因如下:
我们可能在 onload 事件处理函数中编写了用于初始化 UI 的代码;
onload 事件触发时, 浏览器停止 "忙指示器", 并向用户反馈页面已经准备就绪.
部分低版本浏览器 (IE6,IE7,IE8,Safari3,Safari4,Chrome1,Chrome2 等) 只有 onload 事件触发之后才会触发 unload 事件. 有时, 我们会把一些重要的操作和 window 的 unload 事件绑定在一起. 例如, 减少内存泄露的代码. 如果 onload 花费时间太长, 用户可能会离开页面, 那么在这些浏览器中 unload 可能就永远不会执行了.
通常情况下, iframe 中的内容对页面来说不是很重要的(譬如第三方的广告), 我们不应该因为这些内容而延迟 window.onload 事件的触发.
综上, 即使 iframe 是空的, 其开销也会很高, 而且他会阻塞 onload 事件. 所以, 我们应该尽可能避免 iframe 的使用.
七, 图片优化
在大多数网站中, 图片的大小往往能占到一半以上, 所以优化图片能带来更好的效果; 而且, 对图片的优化, 还可以实现再不删减网站功能的条件下实现网站性能的提升.
1, 图像格式
GIF
透明: 允许二进制类型的透明度, 要么完全透明, 要么不透明.
动画: 支持动画. 动画由若干帧组成.
无损: GIF 是无损的
逐行扫描: 生成 GIF 时, 会使用压缩来减小文件大小. 压缩时, 逐行扫描像素, 当图像在水平方向有很多重复颜色时, 可以获得更好的压缩效果.
支持隔行扫描
GIF 有 256 色限制, 所以不适合显示照片. 可以用来显示图形, 但是 PNG8 是用来显示图形的最佳方式. 所以, 一般在需要动画时才用到 GIF.
JPEG
有损
不支持动画和透明
支持隔行扫描
PNG
透明: PNG 支持完全的 alpha 透明
动画: 目前无跨浏览器解决方案
无损
逐行扫描: 和 GIF 类似, 对水平方向有重复颜色的图像压缩比高.
支持隔行扫描
隔行扫描是什么:
网速很慢时, 部分图像支持对那些连续采样的图像进行隔行扫描. 隔行扫描可以让用户在完整下载图像之前, 可以先看到图像的一个粗略的版本, 从而消除页面被延迟加载的感觉.
2,PNG 在 IE6 中的奇怪现象
所有在调色板 PNG 中的半透明像素在 IE6 下会显示为完整的透明.
真彩色 PNG 中的 alpha 透明像素, 会显示为背景色
3, 无损图像优化
PNG 图像优化
PNG 格式图像信息保存在 "块" 中, 对于 web 现实来说, 大部分块并非必要, 我们可以将其删除.
推荐工具: Pngcrush
JPEG 图像优化
剥离元数据(注释, 其他内部信息等)
这些元数据可以安全删除不会影响图片质量.
推荐工具 jpegtran
GIF 转换成 PNG
前面提到 GIF 的功能吃了动画之外, 完全可以用 PNG8 来代替, 所以我们使用 PNG 代替 GIF
推荐工具 ImageMagick
优化 GIF 动画
因为动画里面有很多帧, 并且部分内容在很多帧上都是一样的, 所以我们可以将图像里面连续帧中的重复像素移除.
推荐工具: Gifsicle
4,CSS sprite 优化
如果网站页面较少, 可以将图像放在一个超级 CSS sprite 中
看看 Google 就使用了一个:
最佳实践:
按照颜色合并: 颜色相近的突变组合在一起
避免不必要的空白
元素水平排列: 比竖直排列稍微小点
将颜色限制在 25 种之内(尽量)
先优化单独的图像, 再优化 Sprite
通过控制大小和对齐减少反锯齿的数量.
避免使用对角线渐变, 这种渐变无法被平铺.
IE6 中 alpha 透明图像单独使用 sprite
每 2-3 个像素改变渐变颜色, 而不是每个
避免对图像缩放
如果我们需要一张小的图像, 就没必要在下载一张大的图像之后在 HTML 中将其缩小.
譬如我们需要一个 100*100 的图像, 我们可以现在服务器端改变图像的大小, 这样可以节省下载的流量.
5, 避免对图像缩放
如果我们在页面中用不到大的图像, 就没必要下载一个很大的然后用 css 限制他的大小.
譬如我们需要一个 100*100 的图像, 我们可以现在服务器端改变图像的大小, 这样可以节省下载的流量.
八, 划分主域
在之前我们谈到为了减少 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 地址, 浏览器会将每个主机名当做一个单独的服务端.
另外, 研究表明, 域的数量从一个增加到两个性能会得到提高, 但超过两个时就可能出现负面影响了. 最终数量取决于资源的大小和数量, 但分为两个域是很好的经验.
来源: https://www.cnblogs.com/MarcoHan/p/5297798.html