高性能js-笔记

1. 加载和执行

  • 当浏览器遇到script标签时,会停止处理页面,先执行js代码,然后再继续解析和渲染页面。
  • 使用src加载js的过程中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行,在这过程中,页面渲染和用户交互是阻塞的。
  • js下载和执行会阻塞其它资源的下载,比如图片
  • 逐步加载js文件,不会阻塞浏览器,秘诀在于,在页面加载完成之后才加载js代码,这意味着window的load事件出发后再下载脚本

延迟脚本

  • 1.defer
    当一个带有defer属性的js文件下载时,不会阻塞浏览器的其它进程,因此与其它资源并行下载。
<script defer="defer" src="./defer.js"></script>//alert('defer')
<script>
    alert('script')
</script>
<script>
    alert('1')
</script>
<script>
    window.onload=function() {
        alert('load')
    }
</script>
结果为: script 1 defer load

注意:
defer在src外链js时才起作用,js脚本的执行需要等到文档所有元素解析完成之后,load事件触发执行之前。
多个defer的文件,加载完顺序执行;多个async文件,哪个先加载完执行哪个

  • 2.动态脚本元素
function loadScript(url, callback) {
    var script = document.createElement('script');
    script.type = "text/javascript";
    if(script.readyState) {//IE
        script.onreadystatechange = function() {
            if(script.readyState === 'complete' || script.readyState === 'loaded') {
                script.onreadystatechange = null;
                callback()
            }
        }
    }else {//其他浏览器
        script.onload = function() {
            callback()
        }
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}

文件在该元素被添加到页面时开始下载,文件的下载和执行过程不会阻塞页面其他进程

  • 3.Ajax注入
var xhr = new XMLHttpRequest();;
xhr.open('get','./defer.js',true);
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.getElementsByTagName('head')[0].appendChild(script);
        }
    }
}
xhr.send(null)

小结

  • 将脚本放在底部,</body>之前
  • 合并脚本,减少页面中外链脚本的数量
  • 无阻塞下载js
    • 使用defer或async属性
    • 动态创建script元素
    • 使用xhr下载js并注入页面中

2. 数据存取

管理作用域

  • 内部属性[[Scope]]包含了一个函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链
  • 执行函数时会创建一个执行环境,它的作用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些值按照它们出现在函数中的顺序,被复制到执行环境的作用域链中,这个过程完成一个活动对象就为执行环境创建好了。执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存取数据。当函数执行完毕时,执行环境就被销毁

  • 全局变量总是存在于执行环境作用域链的最末端,可以用局部变量代替全局变量

  • 最好避免使用with语句,因为延长作用域链后访问局部变量将变慢。对于try-catch推荐将错误委托给一个函数处理
try{
    method();
}catch(ex) {
    handleError(ex)
}
  • 闭包性能问题:
function assignEvent(){
        var id = "xdi9592";
        document.getElementById('save-btn').onclick=function(){
        saveDocument(id);
    }
}

当assignEvent函数执行时,一个包含了变量id以及其他数据的活动对象被创建。它成为执行环境作用域链中的第一个对象,而全局对象紧随其后。这个活动对象无法在assignEvent执行后被销毁,因为闭包也引用了它。当闭包执行时会创建一个执行环境,又一个活动对象为闭包自身所创建。

  • 对象成员包括属性和方法。
  • 对象成员搜索会消耗性能

小结

  • 访问字面量和局部变量的速度最快,访问数组元素和对象成员慢
  • 访问局部变量比访问跨作用域变量快
  • 避免使用with和try-catch
  • 嵌套对象成员影响性能
  • 属性或方法在原型链位置越深,访问越慢
  • 将对象保存在变量中

3. DOM编程

  • 减少DOM访问次数,把运算尽量放在ECMAScript这端处理
  • 推荐使用innerHTML更新一大段html
  • html集合一直与文档保持着连接,每次需要最新的信息时,都会重复执行查询过程。如: length属性,所以可以考虑缓存length
  • 访问集合元素时使用局部变量,如var coll = document.getElementById('app')
  • 遍历DOM
function testNextSibling() {//推荐nextSibling来查找DOM节点
    var el = document.getElementById('s'),
        fir = el.firstElementChild,
        name;
    do{
        name = fir.nodeName;
    }while(fir = fir.nextElementSibling);
    return name;
}
function testChildNodes() {
    var el = document.getElementById('s'),
        children = el.children,
        len = children.length,
        name;
    for(var i = 0;i < len;i++) {
        name = children[i].nodeName
    }
    return name;
}
  • 组合查询: var errs = document.querySelectorAll('div.warning,div.notice')
function search() {
   var el = document.getElementById('s'),
        ch = el.firstElementChild,
        className = '',
        arr = [];
    do{
        className = ch.className;
        if(className === 'notice' || className === 'warning') {
            arr.push(ch);
        }
    }while(ch = ch.nextElementSibling);
    return arr;
}
  • 缓存布局信息,比如offsetTop。。。
  • 如果有大量元素使用了:hover,那么会降低响应速度,此问题在ie8中尤为明显
  • 事件委托

重排与重绘

  • DOM树

表示页面结构

  • 渲染树

表示DOM节点如何显示

渲染树中的节点被称为“帧”或“盒”。当DOM变化影响了元素的几何属性,浏览器需要重新计算元素的几何属性,其他元素的几何属性和位置也会因此受到影响。
重排:浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
重绘:完成重排后,浏览器会重新绘制影响的部分到屏幕中。

重排何时发生:
1. 添加或删除可见的DOM元素
2. 元素位置改变
3. 元素尺寸改变
4. 内容改变
5. 页面渲染器初始化
6. 浏览器窗口尺寸改变

最小化重排与重绘:
1. el.style.cssText+= "..."
2. el.className+= "active"
批量修改DOM:
1. 通过改变display属性,临时从文档中移除某元素
2. 在文档外创建并更新一个文档片段,然后把它附加到原列表中。
3. 先cloneNode,然后replaceChild

小结

  • 最小化dom访问次数
  • 用变量缓存dom节点
  • 小心使用html集合,可将它复制到一个数组
  • 使用更快的api如querySelectorAll和firstElementChild
  • 批量修改样式,离线操作dom树,使用缓存,减少访问布局信息的次数
  • 动画中使用绝对定位,使用拖放代理
  • 使用事件委托

4. 算法和流程控制

循环

  • for-in循环明显慢
  • 除非明确需要迭代一个属性数量未知的对象,否则应避免使用for-in循环。如果需要遍历一个数量有限的已知属性列表,使用其他循环类型会更快。
  • 减少每次迭代处理的事物
  • 减少迭代的次数
var i = items.length % 8;
while(i) {
    process(items[i]);
}
i = Math.floor(items.length/8);
while(i) {
    process(item[i--]);
    process(item[i--]);
    process(item[i--]);
    process(item[i--]);
    process(item[i--]);
    process(item[i--]);
    process(item[i--]);
    process(item[i--]);//八个
}
  • 将length属性存在变量中
  • 减少属性查找并反转
for(var i = items.length; i > 0; i--) {
    process(items[i])
}
  • 基于函数的迭代比基于循环的迭代要慢一些
  • 条件较少时用if-else,多时用switch
  • 合并排序数组
    递归:
function merge(left, right) {
    var arr = [];
    while(left.length && right.length) {
        if(left[0] < right[0]) {
            arr.push(left.shift())
        }else {
            arr.push(right.shift())
        }
    }
    return arr.concat(left, right)
}
function mergeSort(items) {
    if(items.length == 1) {
        return items;
    }
    var middle = Math.floor(items.length / 2),
        left = items.slice(0, middle),
        right = items.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

迭代实现

function merge(left, right) {
    var arr = [];
    while(left.length && right.length) {
        if(left[0] < right[0]) {
            arr.push(left.shift())
        }else {
            arr.push(right.shift())
        }
    }
    return arr.concat(left, right)
}

function mergeSort(items) {
    if(items.length == 1) {
        return items;
    }
    var work = [];
    for(var i = 0,len = items.length; i < len; i++) {
        work[i] = [items[i]];
    }
    work.push([])//如果数组是奇数;
    for(var lim = len; lim > 1;lim = (lim+1)/2) {
        for(var j =0, k = 0; k < lim; j++, k += 2) {
            work[j] = merge(work[k], work[k + 1])
        }
        work[j] = [];
    }
    return work[0]
}
  • 重写factorial
function memorize(fundamental, cache) {
    cache = cache || {};
    var shell = function (arg) {
        if(!cache.hasOwnProperty(arg)) {
            cache[arg] = fundamental(arg)
        }
        return cache[arg]
    }
    return shell
}
function factorial(n) {
    if(n == 1) {
        return 1
    }else {
        return n*factorial(n-1)
    }
}
var mem = memorize(factorial, {"0":1,"1":1})

小结

  • 避免使用for-in
  • 减少跌代次数和运算量
  • 浏览器的调用栈大小限制了递归算法

5. 字符串和正则表达式

字符串连接


str = "a" + "b" + "c"
str = ["a", "b", "c"].join("");//避免重复分配内存和拷贝逐渐增大的字符串
str = "a";
str += "b";//稍微比concat好
str = "a";
str = str.concat("b", "c")

6. 快速响应的用户界面

  • 当一个脚本运行时,点击一个按钮将无法看到它被按下的样式,尽管onclick事件处理器会被执行
  • web workers适用于那些处理纯数据,或者与浏览器UI无关的长时间运行的脚本
  • web workers从外部线程中修改dom会导致用户界面出现错误(worker在UI线程之外)
  • 与worker通信
var worker = new Worker('code.js');
worker.onmessage = function(event) {
    alert(event.data);
}
worker.postMessage('nicholas');
//code.js
importScripts("file1.js","file2.js")
self.onmessage = function(event) {
    self.postMessage('hello'+event.data);
}

小结

  • 任何js任务都不应执行超过100毫秒
  • 安排代码延迟执行,将任务分解
  • web workers允许在ui线程外执行js

7. Ajax

  • 使用xhr时,post和get对比:经get请求的数据会被缓存,url长度有限制
  • 后台读取图片并将他们转换为base64编码的字符串 "data:images/jpeg;base64,"+data
  • 对于少量数据而言,一个get请求往服务器只发送一个数据包。而一个post请求要发送一个装载头信息,和一个装载post正文。
  • beacons (new Image()).src = url+'?'+params.join(&) 服务器接收到数据并保存下来,无须向客户端发送任何回馈信息

数据格式

  • json(轻量易于解析的数据格式)
    体积更小,结构占用比例小,数据占用多,解析速度快
  • xml
    极佳的通用性,格式严格,易于验证。但是数据占比低,解析消耗多
  • html
    臃肿复杂,传输量大,解析事件长
  • 自定义格式
    把数据用分隔符链接起来,比较轻量

缓存数据

  • 服务器端设置http头确保响应被缓存
  • 客户端,把获取的信息存储到本地,避免再次请求

小结

  • 减少请求数,可合并js和css,或使用mxhr
  • 缩短页面加载时间,用ajax获取次要文件
  • 确保代码错误不会输出给用户,在服务器端处理错误

8. 编程实践

  • 避免双重求值,每次调用eval()时都要创建一个新的解释器/编译器实例
  • 避免重复工作
function addHandler(target, eventType, handler) {
    if(target.addEventListener) {
        addHandler = target.addEventListener(eventType, handler, false);
    }else if(target.attachEvent) {
        addHandler = target.attachEvent('on'+eventType, handler)
    }
    addHandler(target, eventType, handler)
}
function addHandler = document.body.addEventListener ? 
                    function(target, eventType, handler) {
                        target.addEventListener(eventType, handler, false);
                    }:
                    function(target, eventType, handler) {
                        target.attachEvent("on" + eventType, handler);
                    }
  • 虽然js运行速度慢很容易归咎于引擎,然而引擎通常是处理过程中最快的部分,运行速度慢的实际上是你的代码。
    使用位操作符,原生方法,更快一点

小结

  • 避免使用eval和Function构造器来避免双重求值带来的性能消耗。给setTimeout和setInterval传递函数而不是字符串作为参数
  • 尽量使用直接量创建对象和数组。
  • 避免重复工作
  • 尽量使用原生方法

9. 构建js应用

  • 合并多个js文件,大大降低页面渲染所需的http
  • 预处理js文件
  • js文件压缩
  • js的服务器端http压缩。浏览器发送一个Accept-Encoding头,服务器返回Content-Encoding头。可以理解成编码
  • 缓存js
    • http缓存
    • 客户端缓存
    • h5离线应用缓存

当应用升级时,需要确保用户下载到最新的静态内容。可以通过把改动的静态资源重命名。多数情况下,开发者给文件增加一个版本或开发编号,也可以附加一个校验和

  • 使用内容分发网络cdn。相当于在客户端和服务器间设置镜像缓存

函数缓存

var store = {
    nextId: 1,
    cache: {},
    add: function(fn) {
        if(!fn.id) {
            fn.id = store.nextId++;
            return (!!store.cache[fn.id] = fn);
        }
    }
}

自记忆函数

function isPrime(value) {
    if(!isPrime.answers) isPrime.answers = {};
    if(isPrime.answers[value] != null) return isPrime.answers[value];
    var prime = value != 1;
    for(var i = 2; i < value; i++) {
        if(value % i ==0) {
            prime = false;
            break;
        }
    }
    return isPrime.answers[value] = prime;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值