前端优化常用技术心得
栏目: JavaScript · 发布时间: 6年前
内容简介:从建立http连接开始,到页面展示到浏览器里,经历了加载、执行、渲染,重构的几个阶段。将分享下我自己的心得和其他人的优秀经验。浏览器是友善的客户端,对同域名并发请求是有数量限制,过去浏览器一般是2个,支持H5的一般是6个;并且服务器端是可以关闭请求。 有朋友不理解,为什么不是并发越多越好?举个例子:百万级的PV,并发数量过大会造成什么样的后果? 由此,所有的优化都是基于这个点和单线程而延伸出来的。 所以,前端的资源加载优化有两个方向js文件加载后是否要立即执行?立即执行是否会影响页面渲染?过去浏览器在加
从建立http连接开始,到页面展示到浏览器里,经历了加载、执行、渲染,重构的几个阶段。将分享下我自己的心得和其他人的优秀经验。
加载和执行
浏览器是友善的客户端,对同域名并发请求是有数量限制,过去浏览器一般是2个,支持H5的一般是6个;并且服务器端是可以关闭请求。 有朋友不理解,为什么不是并发越多越好?举个例子:百万级的PV,并发数量过大会造成什么样的后果? 由此,所有的优化都是基于这个点和单线程而延伸出来的。 所以,前端的资源加载优化有两个方向
- 开源 增加域名 既然同域名不能太多,那么就多域名;简单来说就是cdn,可以是第三方,也可以自己多弄几个二级域名
-
节流
资源压缩、按需加载 同域名内的文件充分的进行压缩,比如:本来2M的资源,如果压缩到1M以下(去除空格,gzip等)速度的提升就是50%;再有现在spa是将文件合并后进行压缩和打包,如果文件总体并不大,性能不会有太大影响;一旦开发中引入的UI库或第三方插件多了,总文件体量也不在少数;就有了:按需加载、延时加载的用武之地。比如在webpack打包的时候从template的html中单独加入某个css或js;更有webpack-http-require的库。
当然,图片也需要做很多相应的处理
- css实现效果(按钮、阴影等)
- 压缩尺寸和size
- sprite合并
- svg、toff字体图
- canvas绘制大图(地图相关)
阻塞性优化
js文件加载后是否要立即执行?立即执行是否会影响页面渲染?过去浏览器在加载和执行js文件时是阻塞状态,就是按照栈原理一个个来;所以,原来要求把js文件放到html代码底部前,现代浏览器某种程度上解决了并行加载的问题,也可以进行预加载,但是执行之后会否对页面造成重排?所以要灵活应用dns-prefetch、preload和defer|async,当然defer和async不是所有浏览器都生效,webkit核心的就没生效。
<!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> 复制代码
js执行优化
- 作用域优化,变量层级不要太深或嵌套太多,最好是本级;大家在看各大框架或库的时候,经常可以看到这种写法:
(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 } } 复制代码
- 循环优化 循环是编程中最常见的结构,优化循环是性能优化过程中很重要的一部分。一个循环的基本优化步骤如下:
减值迭代——大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加有效。 简化终止条件——由于每次循环过程都会计算终止条件,故必须保证它尽可能快,即避免属性查找或其它O(n)的操作。 简化循环体——循环体是执行最多的,故要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。 使用后测试循环——最常用的for和while循环都是前测试循环,而如do-while循环可以避免最初终止条件的计算,因些计算更快。
for(var i = 0; i < values.length; i++) { process(values[i]); } 复制代码
优化1:简化终止条件
for(var i = 0, len = values.length; i < len; i++) { process(values[i]); } 复制代码
优化2:使用后测试循环(注意:使用后测试循环需要确保要处理的值至少有一个)
var i values.length - 1; if(i > -1) { do { process(values[i]); }while(--i >= 0); } 复制代码
- 展开循环
当循环的次数确定时,消除循环并使用多次函数调用往往更快 当循环的次数不确定时,可以使用Duff Service来优化,基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。如下:
// Jeff Greenberg for JS implementation of Duff's Device // 假设:values.length > 0 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.ceil(values.length / 8); var startAt = values.length % 8; var i = 0; do { switch(startAt) { case 0 : process(values[i++]); case 7 : process(values[i++]); case 6 : process(values[i++]); case 5 : process(values[i++]); case 4 : process(values[i++]); case 3 : process(values[i++]); case 2 : process(values[i++]); case 1 : process(values[i++]); } startAt = 0; }while(--iterations > 0); 复制代码
如上展开循环可以提升大数据集的处理速度。接下来给出更快的Duff装置技术,将do-while循环分成2个单独的循环。(注:这种方法几乎比原始的Duff装置实现快上40%。)
// Speed Up Your Site(New Riders, 2003) 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; 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); 复制代码
针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。
- 避免双重解释
当JS代码想解析JS代码时就会存在双重解释惩罚,当使用eval()函数或是Function构造函数以及使用setTimeout()传一个字符串时都会发生这种情况。如下
eval("alert('hello world');"); // 避免 var sayHi = new Function("alert('hello world');"); // 避免 setTimeout("alert('hello world');", 100);// 避免 复制代码
以上代码是包含在字符串中的,即在JS代码运行的同时必须新启运一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,故这种代码要比直接解析要慢。以下这几个例子,除了极少情况下eval是必须的,应尽量避免使用上述。对于Function构造函数,直接写成一般的函数即可。对于setTimeout可以传入函数作为第一个参数。如下:
alert('hello world'); var sayHi = function() { alert('hello world'); }; setTimeout(function() { alert('hello world'); }, 100); 复制代码
总之,若要提高代码性能,尽可能避免出现需要按照JS解释的代码。
-
性能的其它注意事项 原生方法更快——只要有可能,使用原生方法而不是自已用JS重写。原生方法是用诸如C/C++之类的编译型语言写出来的,要比JS的快多了。很多人认为自定义的 排序 会比sortby更快,事实比对效果还是原生方法更加优秀。 switch语句较快——若有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码,还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化。 位运算较快——当进行数学运算时,位运算操作要比任何布尔运算或算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能,诸如取模,逻辑与和逻辑或也可以考虑用位运算来替换。
-
最小化语句数 JS代码中的语句数量也会影响所执行的操作的速度,完成多个操作的单个语句要比完成单个操作的多个语句块快。故要找出可以组合在一起的语句,以减来整体的执行时间。这里列举几种模式
1.多个变量声明
// 避免 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(); 复制代码
2.插入迭代值
// 避免 var name = values[i]; i++; // 提倡 var name = values[i++]; 复制代码
3.使用数组和对象字面量,避免使用构造函数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 }; 复制代码
4.优化DOM交互 在JS中,DOM无疑是最慢的一部分,DOM操作和交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一个部分,故理解如何优化与DOM的交互可以极大提高脚本完成的速度。后面会针对性说明
数组存储
计算机科学中有个经典问题:通过改变数据存储的位置来获得最佳的读写性能,数据存储的位置关系到代码执行过程中数据的检索速度。在JS中这个问题相对简单,因为只有4种方案。
- 字面量 字面量就代表自身,不存储在特定位置。JS字面量有:字符串、数字、布尔、对象、数组、函数、正则表达式和特殊的null、undefined
- 本地变量 使用var定义的数据存储单元
- 数组元素 存储在JS对象内部,以数字作为索引
- 对象成员 存储在JS对象内部,以字符串作为索引 每一种数据存储的位置都有不同的读写消耗。大多情况下差不多,数组和对象代价稍高一些,具体表现看浏览器的性能和js解释器。尽量使用字面量和局部变量,减少数组项和对象成员的使用。
作用域
理解作用域概念是JS和核心关键,不仅从性能还得从功能的角度。简单说:生效的范围(域),哪些变量可以被函数访问,this的赋值,上下文(context)的转换。说到作用域就不能绕开作用域链。理解了作用域链和标识符就理解了作用域。
作用域链和标识符解析
每个函数都是Function对象的实例,Function对象和其它对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问而仅供JS引擎存取的内部属性。其中一个内部属性是[[Scope]],有ECMA-262标准第三版定义
内部属性[[Scope]]包含了一个函数被创建的作用域中对象的集合。这个集合被称为函数的作用域链,它决定那些数据能被函数访问,函数作用域中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,他的作用域链会被创建此函数的作用域中可访问的数据对象所填充。例如:
function fn(a,b){ return res = a*b; } 复制代码
当fn创建时,它的作用域链中插入了一个对象变量,这个全局对象代表着所有在全局范围内定义的变量。该全局对象包含window、navigator、document等。fn执行的时候就会用到作用域,并创建执行环境也叫执行上下文。它定义了一个函数执行时的环境,即便是同一个函数,每次执行都创建新的环境,函数执行完毕,环境就销毁。 每个环境都要根据作用域和作用域链解析参数、变量。可以理解为作用域链好比一个堆栈,栈顶就是当前的活动对象(环境创建时函数[[Scope]]属性中的对象集合)大多情况也可以理解为函数内部定义的局部变量。
而闭包的是根据JS允许函数访问局部作用域之外的数据,虽然会带来性能问题,因为执行环境虽然销毁,但激活的对象依然存在,所以可以缓存变量,从而不用全局对象。适用
对象
属性和方法,两者都是对象的成员,引用了函数就是方法,非函数就是属性。为什么对象访问慢呢?因为原型链问题。
原型和原型链
直接看代码
function fun(name,age){ this.name = name+''; this.age = age } fun.prototype.getName = function(){ return this.name; } var fn = new fun(); true = (fn instanceof fun) //true true = (fn instanceof Object) fn.__proto__ = fun.prototype /* * fun的原型方法 __proto__ = null hasOwnProperty = (function) isPrototypeOf = (function) propertyIsEnumerable = (function) toLocaleString = (function) toString = (function) valueOf = (function) */ 复制代码
平时普通变量也是这样一级级向上直到根(window)下,没有此变量或属性或方法,才返回undefined;
DOM编程
DOM操作代价高昂,这是web application最常见的性能瓶颈,Document Oject Module(DOM)是独立于语言的,用于操作xml和html文档的的程序接口,而且在浏览器中是通过js实现的。 各个公司的浏览器渲染和js解释引擎都不同,著名的V8相信大家都知道,是一个js引擎;但Chrome的渲染是WebCore。每个浏览器都有两套解释器,并相对独立。这就意味着每次操作都需要(V8<=>WebCore)==>Browser 两个解释器都是需要连接和通讯成本。减少两解释器通讯并减少页面改变的频率就是优化的方向。
重绘repaint和重排reflow
DOM树里的每个需要显示的节点在渲染树中至少存在一个对应的节点,隐藏的(display:none)的DOM元素则没有;渲染树的节点被称为帧(frames) 盒(boxes),DOM和渲染树构建完毕,浏览器就开始绘制页面元素(paint)
何时发生重重绘?当页面的几何属性发生变化,影响到现有的文档流需要重新调整页面排版的时候。举几个例子:
- 添加或删除可见的DOM元素;
- DOM元素位置改变;
- DOM元素尺寸改变:容器padding、border、margin属性变化等;
- 容器内的内容变化导致宽高变化:文本行数变多(少)、图片坍塌、图片被另一张大图替换
- 浏览器窗口初始化和尺寸改变 重排结束后,就需要重绘。所以,尽可能的避免重排的产生,为了避免或少的进行重绘和重排,需要尽可能少的访问某些变量:
offsetTop、offsetLeft、offsetWidth、offsetHeight scrollTop、scrollLeft、scrollWidth、scrollHeight clientTop、clientLeft、clientWidth、clientHeight getComputedStyle() (currentStyle in IE) function scroller(){ var H = document.body.offsetHeight || scrollHeight return function(){ var args = arguments,ct = this; // your code } } 复制代码
为了最小和最少的影响到重绘和重排,应该尽可能少的修改DOM,访问影响重排的属性。如果非要修改,尽量尊从三个步骤: 1.元素脱离文档流 2.一次性应用多重改变 3.恢复到文档流中 第一和第三步都会发生重排,所以核心的还是第二步。现在虚拟dom大:fire:,我们稍微了解下基础做法即可。 一次性更新的几种方式:字符串或数组.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)即可不用操心这些 :)
算法和流程控制
代码的整体结构是影响运行速度的主要因素之一,数量与运行速度不一定成正比。组织结构、思路和执行效率才是核心!! JS属于ECMA的范畴,是一种脚本类语言,很多流程上的控制,工程化的思路是从 java 、c等语言上借鉴过来的,所以,知道后端语言的编码和工程化有助于我们加深理解。
Loop循环
- for
for(var i=0;i<10;i++){ // code } 复制代码
倒序可以在大数据量时提高少许效率,i<obj.length;i-- 2. while 前置循环
var i=0; while(i<10){ // code i++; } 复制代码
后置循环
var i=0; do{ // code }while(i++<10) 复制代码
- for - in
for(var prop in object){ // code } 复制代码
除了for-in循环其它效率所差不多,那么能够提高效率的点也就两个
- 每次迭代处理的事务
- 迭代的次数 一般数组array遍历写法的循环中,每次都会有如下操作: 1.在控制条件中查找一次属性array.length 2.在控制条件中执行一次数值比较 (i<array.length) 3.比较循环条件是否满足,(i<array.length === true) 4.一次自增或自减操作 (i++||i--) 5.数组、对象查找 array[i] 6.具体事务处理 将length提前获取并存到变量中可以减少两步(1和2),当循环复杂度为O(n)时,减少每次迭代的工作量是最有效的,当复杂度大于O(n),需要着重减少迭代次数。
条件判断
if-else vs switch 条件数量越多,switch的迭代效率会更高;当只有二选一或简单判断if-else的易读性更好。在实际coding中,如果只有二选一,有些情况甚至可以不用if-else,采用三目运算:result = (true||false)?condition0:condition1;还有将最可能发生的条件写到“if()”里面,减少判断次数,延伸开来就是if-elseif的判断可能性要从大到小。甚至可以采用二分法:
//---假设某参数的值非正即负,或查询二叉树,或查询不同SP的手机号 if(parse>0){ if(parse>10){ //code }else if(parse<5&&parse>1){ //code }else{ } }else{ //code 负数处理 } 复制代码
当然,这是个简单的栗子,还有很多其它的方式可以在代码中引入算法,提高效率,比如星期几的输出
function getweek(){ var w = ['日','一','二','三','四','五','六'], now = new Date(), d = now.getDay(); return '星期'+w[d]; } 复制代码
可以将字符串、变量、方法,存到数组或对象中。因为是引用,效率也非常快
递归
1.递归
//---阶乘 function facttail(n){ if(n==0){ return 1; }else{ return n*facttail(n-1); } } //---幂次方 function fn(s,n){ if(n==0){ return 1; }else{ return s*fn(s,n-1); } } 复制代码
但是递归如果结束条件不明确就会导致一直运行,页面长时间不响应。处于假死状态!!而且,每个浏览器的“调用栈”都是有上限的。有兴趣的可以自己实验。为避免此问题,除了明确结束条件,还可以采用“尾递归” 2.尾递归
//---阶乘 function facttail(n,res){ if(n<0){ return 0; }else if(n==0){ return 1; }else if(n==1){ return res; }else{ return facttail(n-1, n*res); } } //---幂次方 function fn(s,n){ if(n==0){ return 1; }else{ return s*fn(s,n-1); } } 复制代码
缓存记忆
利用闭包特性,某个方法内部可以存储计算过的数据或变量,比如阶乘函数重写
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; } 复制代码
字符串和正则
*?+ 这个部份也需要蛮长的篇幅,占坑先。。。
快速响应的页面
老生常谈的内容,如果让页面秒开;可优化的点有哪些?服务器直渲、首页优化、组件懒加载、bigpipe、性能监控和针对性优化等等。
浏览器线程
先挖坑,我会再专门的文章里共享一点自己的心得
event loop
阮一峰老师说的更好,请移步链接This link
Web Workers
从Google的Gears插件提出了Worker Pool API,它就是Web Workers的“原型”,最初希望能够增强浏览器的功能,比如支持离线浏览(离线访问缓存页面,重新上线后提交离线操作),但(2015/11)已经被弃用了。HTML5开始Web Workers API被分离出去,成立单独的规范。自此,我们可以将计算、编解码、真正的异步请求等放到Web Workers里.
- 运行环境 worker的global context并不是window,而是self,self也提供一系列接口,包括self.JSON、self.Math、self.console等等,最直观的区别是document对象没了,但location(readonly)、navigator还在;所以DOM访问也不存在。要启用它只能在创建一个独立的js文件并通过下面的方式调用
// html中直接写 var worker = new Worker('worker.js') // 或通过主页面的js文件调用,例如:main.js //---主页面 if (window.Worker) { var worker = new Worker('worker.js'); var data = {a: 1, b: [1, 2, 3], c: 'string'}; worker.postMessage(data); worker.onmessage = function(e) { console.log('main thread received data'); console.log(e.data); // 接到消息立即停止worker,onerror将不会触发 // worker.terminate(); // terminate之后收不到后续消息,但post不报错 // worker.postMessage(1); } worker.onerror = function(err) { console.log('main thread received err'); console.log(err.message); // 阻止报错 err.preventDefault(); } } 复制代码
worker.js
//---处理js,可以引入其它依赖 // importScripts('lib.js'); // importScripts('a.js', 'b.js'); onmessage = function(e) { console.log(self); // 看看global变量身上有些什么 var data = e.data; console.log('worker received data'); console.log(data); var res = data; res.resolved = true; postMessage(res); setTimeout(function() { throw new Error('error occurs'); // close,立即停止,相当于主线程中的worker.terminate() // close(); }, 100); }; 复制代码
- 通信 主线程和worker线程收发消息方式一致(postMessage发,onmessage/onerror收,数据从MessageEvent的data属性取),PS:线程之间传递的是值copy,而不是共享引用
- 加载外部文件 importScripts可以引入其它js文件,外部文件中的全局变量将被粘在self上,worker里可以直接引用。importScripts是同步的,下载并执行完毕后执行下一行,所以,需要注意阻塞性问题。 应用范围:
- 音频/视频解码 如果尝试过audioContext.decodeAudioData之类的操作就会发现,我们迫切需要一个能“干重活”的后台线程
- 图片预处理 比如头像上传前的裁剪,甚至添加水印、拼合、添马赛克,如果在客户端能够完成,就能避免大量的临时文件传输
- 排序等数据处理算法 减轻服务器压力,遇到超大数据或超过200ms不能处理完毕的方法 数据过大的JSON对象,超出允许时间
//---main var worker = new Worker('worker.js') worker.onmessage = (e)=>{ var jsonData = e.data // 回传回来的数据 evaluateData(jsonData) } worker.postmessage(jsonText) ///---worker self.onmessage = (e){ var jsonText = e.data // main传过来的数据 var jsonData = JSON.parse(jsonText) // 解析转换 self.postMessage(jsonData) } 复制代码
- 客户端模版 比如markdown,或者服务端返回JSON,客户端拿到后交给后台线程解析并应用模版HTML生成页面,这些操作都由客户端完成的话,需要传输的东西就更少了
- 共享worker 必须是同源!必须是同源!必须是同源!
//---main var sWorker = new SharedWorker('worker.js') sWorker.port.start() //---first first.onchange = function() { sWorker.port.postMessage([first.value,second.value]); console.log('Message posted to worker'); } //---first second.onchange = function() { sWorker.port.postMessage([first.value,second.value]); console.log('Message posted to worker'); } sWorker.port.onmessage = function(e) { result1.textContent = e.data; console.log('Message received from worker'); } 复制代码
通过这种方式建立前端的“类似”多线程
以上所述就是小编给大家介绍的《前端优化常用技术心得》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。