前端优化常用技术
从建立 http 连接开始,到页面展示在浏览器中,一共经过了加载,执行,渲染,重构等几个阶段。
加载
PV(访问量):即 Page View,页面刷新一次算一次。也叫并发量。
UV(独立访客):即 Unique Visitor,一个客户端(电脑,手机)为一个访客
比如:百万级的 PV,并发量过大怎么办 ?
由此,前端所有的优化都是基于这个点和单线程而延伸出来的。所以,前端的资源加载优化有两个方向:
- 开源
增加域名,简单来说就是 cdn。
- 节流
资源压缩,去除空格,gzip 等。
一旦开发中引入的 UI 库或第三方插件多了,总文件体量也不在少数;就有了:按需加载、延时加载的用武之地。
比如在 webpack 打包的时候从 template 的 html 中单独加入某个 css 或 js;
另外,图片也需要做很多相应的处理:
- css 实现效果(按钮、阴影等)
- 压缩尺寸和 size
- sprite 合并
- svg、toff 字体图
- canvas 绘制大图(地图相关)
阻塞性优化
js 页面加载之后是否要立即执行?立即执行是否会影响页面渲染?
过去浏览器在加载和执行 js 文件时是阻塞状态,就是按照栈原理一个一个来;所以,原来要求把 js 文件放到 html 代码底部前。
现代浏览器某种程度上解决了并行加载的问题,也可以进行预加载,但是执行之后会否对页面造成重排?
所以要灵活应用 dns-prefetch、preload 和 defer|async。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo</title>
<link rel="dns-prefetch" href="//cdn.com/">
<link rel="preload" href="//js.cdn.com/currentPage-part1.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part2.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part3.js" as="script">
<link rel="prefetch" href="//js.cdn.com/prefetch.js">
</head>
<body>
<!-- html code -->
<script type="text/javascript" src="//js.cdn.com/currentPage-part1.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part2.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part3.js" defer></script>
</body>
</html>
## 执行优化
作用域优化
- 变量层级不要嵌套太深。
(function(w, d) {})(window, document);
// 目的就是如此,再比如说的缓存某个变量或对象
function check() {
var d = document,
t = document.getElementById("t"),
l = t.children;
for (let i = 0; i < l; i++) {
//code
}
}
循环优化
- 简化终止条件
for (var i = 0; i < values.length; i++) {
process(values[i]);
}
for (var i = 0, len = values.length; i < len; i++) {
process(values[i]);
}
- 展开循环
// 针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。
function process(v) {
alert(v);
}
var values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
var iterations = Math.floor(values.length / 8); // 商
var leftover = values.length % 8; // 余数
var i = 0;
// 一共循环的次数 = 余数的次数 + 商*除数(8)的次数
if (leftover > 0) {
do {
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);
避免双重解释
eval("console.log('hello world');"); // 避免
var sayHi = new Function("console.log('hello world');"); // 避免
setTimeout("console.log('hello world');", 100); // 避免
/**
* 以上代码是包含在字符串中的,即在JS代码运行的同时必须新启运一个解析器来解析新的代码。
* 实例化一个新的解析器有不容忽视的开销,故这种代码要比直接解析要慢。
* 正确的应该这么做:
*/
console.log("hello world");
var sayHi = function() {
console.log("hello world");
};
setTimeout(function() {
console.log("hello world");
}, 100);
最小化语句数
- 多个变量声明
// 避免
var i = 1;
var j = "hello";
var arr = [1, 2, 3];
var now = new Date();
// 提倡
var i = 1,
j = "hello",
arr = [1, 2, 3],
now = new Date();
- 插入迭代值
// 避免
var name = values[i];
i++;
// 提倡
var name = values[i++];
- 使用数组和对象字面量,避免使用构造函数 Array(),Object()
// 避免
var a = new Array();
a[0] = 1;
a[1] = "hello";
a[2] = 45;
var o = new Obejct();
o.name = "bill";
o.age = 13;
// 提倡
var a = [1, "hello", 45];
var o = {
name: "bill",
age: 13
};
性能的其它注意事项
- 原生方法更快
用诸如 C/C++之类的编译型语言写出来的,要比 JS 的快多了。
- switch 语句较快
如果有一系列的复杂的 if-else 语句,可以转换成单个的 switch 语句会更快,还可以通过 case 语句按照最可能的到最不可能的顺序进行组织,进一步优化代码。
数据存储
通过改变数据存储的位置来获取最佳的读写性能。
- 字面量 字面量就代表自身,不存储在特定位置。JS 字面量有:字符串、数字、布尔、对象、数组、函数、正则表达式和特殊的 null、undefined
- 本地变量 使用 var 定义的数据存储单元
- 数组对象 存储在 JS 对象内部
- 对象成员 存储在 JS 对象内部
尽量使用字面量和局部变量,减少数组项和对象成员的使用。
作用域
生效的范围(域),哪些变量可以被函数访问,this 的赋值,上下文(context)的转换。
function fn(a, b) {
return (res = a * b);
}
当 fn 被创建时,它的作用域链(内部属性[[Scope]])中插入了一个对象变量, 这个全局对象代表着在全局范围内定义的所有变量。
全局对象包括:window、navigator、document 等。
fn 执行的时候就会用到作用域,并创建执行环境也叫执行上下文。它定义了一个函数执行时的环境,即便是同一个函数,每次执行都创建新的环境,函数执行完毕,环境就销毁。
每个环境都要根据作用域和作用域链解析参数,变量。
而闭包的是根据 JS 允许函数访问局部作用域之外的数据,虽然会带来性能问题,因为执行环境虽然销毁,但激活的对象依然存在,所以可以缓存变量,从而不用全局对象。
原型和原型链
function fun(name, age) {
this.name = name + "";
this.age = age;
}
fun.prototype.getName = function() {
return this.name;
};
var fn = new fun();
fn instanceof fun; // true
fn instanceof Object; // true
fn.__proto__ = fun.prototype;
/** fun的原型方法
__proto__ = null
hasOwnProperty = (function)
isPrototypeOf = (function)
propertyIsEnumerable = (function)
toLocaleString = (function)
toString = (function)
valueOf = (function)
*/
重排 reflow 和重绘 repaint
会导致重排重绘的情况:
- 添加或删除可见元素
- 元素的位置发生改变
- 元素的尺寸发生改变
- 容器内容发生变化导致元素的宽高发生改变
- 浏览器窗口初始化和尺寸改变
避免或减少发生重排和重绘的方法:
- 尽可能少的访问某些变量
// offsetTop、offsetLeft、offsetWidth、offsetHeight
// scrollTop、scrollLeft、scrollWidth、scrollHeight
// clientTop、clientLeft、clientWidth、clientHeight
function scroller() {
var H = document.body.offsetHeight || scrollHeight;
return function() {
var rgs = arguments,
ct = this;
// you core
};
}
- 字符串或数组.join(’’) innerHTML 方式
- createElement 最后 appendChild
- document.createDocumentFragment,cloneNode 需要改变的节点到缓存节点中,改完替换
function move2RB() {
var dom = document.getElementById("id"),
curent = dom.style.top;
while (curent < 500) {
curent++;
dom.style.cssText = "left:" + curent + "px; top:" + curent + "px";
// 不要写成每次都去获取,left=dom.style.left再加1,甚至是dom.style.left = (pareSint(dom.style.left,10)+1)+'px'这种写法,直接改变className也是可以的。
}
}
总的来说:少访问 DOM,在 js 里处理计算完了再一次性修改,善用缓存和原生 API;用现在的三大框架(angular、react、vue)即可不用操心这些
算法和流程控制
循环
- 倒序循环 for(var i=10;i>0;i–){}
- 后置循环 do{}while(i++<10)
- for-in
条件判断
-
switch 代替 if-else
-
三目运算
-
判断可能性从大到小
-
将字符串、变量、方法存到数组或对象中
function getweek() {
var w = ["日", "一", "二", "三", "四", "五", "六"],
now = new Date(),
d = now.getDay();
return "星期" + w[d];
}
递归
// 阶乘
function facttail(n) {
if (n == 0) {
return 1;
} else {
return n * facttail(n - 1);
}
}
console.log(facttail(5, 1));
// 幂次方
function fn(s, n) {
if (n == 0) {
return 1;
} else {
return s * fn(s, n - 1);
}
}
console.log(fn(2, 3));
利用闭包缓存数据
某个方法内部可以存储计算过的数据或变量:
function memfacttail(n) {
if (!memfacttail.cache) {
memfacttail.cache = {
"0": 1,
"1": 1
};
}
if (!memfacttail.cache.hasOwnProperty(n)) {
memfacttail.cache.n = n * memfacttail(n - 1);
}
return memfacttail.cache.n;
}
console.log(memfacttail(4)); // 4*3*2*1 = 24;