三、针对操作DOM的性能优化方法总结

为了减少DOM操作对页面性能产生的影响,在实现页面的交互效果时一定要注意一下几点:


ES6 标准之前的 JavaScript 异步函数

在深入学习 async 和 await 之前,我们需要先理解 Promise。为了领会
Promise,我们需要回到普通回调函数中进一步学习。

Promise 是在 ES6 中引入的,并促使在编写 JavaScript
的异步代码方面,实现了巨大的提升。从此编写回调函数不再那么痛苦。

回调是一个函数,可以将结果传递给函数并在该函数内进行调用,以便作为事件的响应。同时,这也是JS的基础。

function readFile(‘file.txt’, (data) => { // This is inside the
callback function console.log(data) }

1
2
3
4
function readFile(‘file.txt’, (data) => {
    // This is inside the callback function
    console.log(data)
}

这个函数只是简单的向文件中记录数据,在文件完成之前进行读取是不可能的。这个过程似乎很简单,但是如果想要按顺序读取并记录五个不同的文件,需要怎么实现呢?

没有 Promise
的时候,为了按顺序执行任务,就需要通过嵌套回调来实现,就像下面的代码:

// This is officially callback hell function combineFiles(file1, file2,
file3, printFileCallBack) { let newFileText = ” readFile(string1,
(text) => { newFileText += text readFile(string2, (text) => {
newFileText += text readFile(string3, (text) => { newFileText += text
printFileCallBack(newFileText) } } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This is officially callback hell
function combineFiles(file1, file2, file3, printFileCallBack) {
    let newFileText = ”
    readFile(string1, (text) => {
        newFileText += text
        readFile(string2, (text) => {
            newFileText += text
            readFile(string3, (text) => {
                newFileText += text
                printFileCallBack(newFileText)
            }
        }
    }
}

这就很难推断函数下面会发生什么,同时也很难处理各种场景下发生的错误,比如其中某个文件不存在的情况。

Speedometer 测试

Speedometer 模拟用户在Web应用程序上的操作(具体来说,将项目添加到 to-do
列表中)并测量他们所花费的时间。在这里测试一下。

得分越高的浏览器越好。

图片 1

图片 2

优胜者:谷歌浏览器

3.操作DOM前,先把DOM节点删除或隐藏

JavaScript

var list1 = $(“.list1”); list1.hide(); for (var i = 0; i < 15000;
i++) { var item = document.createElement(“li”);
item.append(document.createTextNode(‘0’)); list1.append(item); }
list1.show();

1
2
3
4
5
6
7
8
var list1 = $(".list1");
list1.hide();
for (var i = 0; i < 15000; i++) {
    var item = document.createElement("li");
    item.append(document.createTextNode(‘0’));
    list1.append(item);
}
list1.show();

display属性值为none的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。


页面rendering时间对比:
下图为同样的循环次数下未隐藏节点直接进行DOM操作的rendering时间(图一)和隐藏节点再进行DOM操作的rendering时间(图二)

图片 3

图片 4

由对比图可以看出,总时间、js执行时间以及rendering时间都明显减少,并且避免了painting以及其他的一些操作。

ES2017 异步函数现已正式可用

2017/08/22 · JavaScript
· ES2017,
异步

原文出处: ERIC
WINDMILL   译文出处:葡萄城控件   

ES2017标准已于2017年6月份正式定稿了,并广泛支持最新的特性:异步函数。如果你曾经被异步
JavaScript 的逻辑困扰,这么新函数正是为你设计的。

异步函数或多或少会让你编写一些顺序的 JavaScript 代码,但是却不需要在
callbacks、generators 或 promise 中包含你的逻辑。

如下代码:

function logger() { let data = fetch(”)
console.log(data) } logger()

1
2
3
4
5
function logger() {
    let data = fetch(‘http://sampleapi.com/posts’)
    console.log(data)
}
logger()

这段代码并未实现你的预期。如果你是在JS中编写的,那么你可能会知道为什么。

下面这段代码,却实现了你的预期。

async function logger() { let data = await
fetch(‘http:sampleapi.com/posts’) console.log(data) } logger()

1
2
3
4
5
async function logger() {
    let data = await fetch(‘http:sampleapi.com/posts’)
    console.log(data)
}
logger()

这段代码起作用了,从直观上看,仅仅只是多了 async 和 await 两个词。

Chrome仍然是胜利者

不幸的是,对于Mozilla来说,Chrome至少在现在看起来仍然是领先者。

但实际上,量子浏览器不是金属块。这是一个功能强大,速度快的浏览器,有创新的书签功能和一个充满创意插件的库。经过
Mozilla 的开发人员在未来几个月内对 Quantum 改进,它有可能赶上Chrome。

编译自:mashable

2 赞 1 收藏 1
评论

图片 5

2.1 导致页面重排的一些操作:

  • 内容改变
    • 文本改变或图片尺寸改变
  • DOM元素的几何属性的变化
    • 例如改变DOM元素的宽高值时,原渲染树中的相关节点会失效,浏览器会根据变化后的DOM重新排建渲染树中的相关节点。如果父节点的几何属性变化时,还会使其子节点及后续兄弟节点重新计算位置等,造成一系列的重排。
  • DOM树的结构变化
    • 添加DOM节点、修改DOM节点位置及删除某个节点都是对DOM树的更改,会造成页面的重排。浏览器布局是从上到下的过程,修改当前元素不会对其前边已经遍历过的元素造成影响,但是如果在所有的节点前添加一个新的元素,则后续的所有元素都要进行重排。
  • 获取某些属性
    • 除了渲染树的直接变化,当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:offsetTopoffsetLeft
      offsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeight
      clientTopclientLeftclientWidthclientHeightgetComputedStyle()
  • 浏览器窗口尺寸改变
    • 窗口尺寸的改变会影响整个网页内元素的尺寸的改变,即DOM元素的集合属性变化,因此会造成重排。

Promise 改善了这种情况

这正是 Promise 的优势所在,Promise 是对还未产生的数据的一种推理。Kyle
Simpson 将 Promise 解释为:就像在快餐店里点餐一样。

  • 点餐
  • 为所点的午餐付费,并拿到排队单号
  • 等待午餐
  • 当你的午餐准备好了,会叫你的单号提醒你取餐
  • 收到午餐

正如上面的这种场景,当你等餐时,你是无法吃到午餐的,但是你可以提前为吃午餐做好准备。你可以进行其它事情,此时你知道午餐就要来了,虽然此刻你还无法享用它,但是这个午餐已经“promise”给你了。这就是所谓的
promise,表示一个最终会存在的数据的对象。

readFile(file1) .then((file1-data) => { /* do something */ })
.then((previous-promise-data) => { /* do the next thing */ })
.catch( /* handle errors */ )

1
2
3
4
readFile(file1)
    .then((file1-data) => { /* do something */ })
    .then((previous-promise-data) => { /* do the next thing */ })
    .catch( /* handle errors */ )

上面是
Promise 语法。它主要的优点就是可以将队列事件以一种直观的方式链接在一起。虽然这个示例清晰易懂,但是还是用到了回调。Promise
只是让回调显得比较简单和更加直观。

测试 Ares-6

Ares-6 测量浏览器运行 Javascript
函数的速度,包括一些数学函数。你可以点击这里查看的细节。

得分较低代表运行速度快。

图片 6

图片 7

如你所见,当涉及到复杂的Javascript函数的运行速度时,Chrome比Firefox快。

优胜者:谷歌浏览器

1.5 优化前后CPU占用对比

优化前文字滚动时的timeline
图片 8


优化后文字滚动时的timeline
图片 9


优化前js的CPU占用率较高,而优化后占用CPU的主要为渲染时间,因为优化后的代码只是控制了节点的显示和隐藏,所以在js上消耗较少,在渲染上消耗较大。

要点和细节

相信我们已经感受到了 asyns 和 await
的美妙之处,接下来让我们深入了解一下细节:

  • async 和 await 建立在 Promise 之上。使用 async,总是会返回一个
    Promise。请记住这一点,因为这也是容易犯错的地方。
  • 当执行到 await 时,程序会暂停当前函数,而不是所有代码
  • async 和 await 是非阻塞的
  • 依旧可以使用 Promise helpers,例如 Promise.all( )

正如之前的示例:

async function logPosts () { try { let user_id = await
fetch(‘/api/users/username’) let post_ids = await
fetch(‘/api/posts/<code>${user_id}’) let promises =
post_ids.map(post_id => { return fetch(‘/api/posts/${post_id}’) }
let posts = await Promise.all(promises) console.log(posts) } catch
(error) { console.error(‘Error:’, error) } }</code>

1
2
3
4
5
6
7
8
9
10
11
12
13
async function logPosts ()  {
    try {
        let user_id = await fetch(‘/api/users/username’)
        let post_ids = await fetch(‘/api/posts/<code>${user_id}’)
        let promises = post_ids.map(post_id => {
            return  fetch(‘/api/posts/${post_id}’)
        }
        let posts = await Promise.all(promises)
        console.log(posts)
    } catch (error) {
        console.error(‘Error:’, error)
    }
}</code>
  • await 只能用于声明为 async 的函数中
  • 因此,不能在全局范围内使用 await

如下代码:

// throws an error function logger (callBack) { console.log(await
callBack) } // works! async function logger () { console.log(await
callBack) }

1
2
3
4
5
6
7
8
9
// throws an error
function logger (callBack) {
    console.log(await callBack)
}
 
// works!
async function logger () {
    console.log(await callBack)
}

谷歌浏览器 VS 火狐量子:哪一个更快呢?

2017/11/22 · 基础技术 ·
1 评论 ·
浏览器

原文出处:
mashable   译文出处:oschina   

Firefox网页浏览器有一个很有趣的特性,就是它会运行你想要的所有的标签。但是,它碰到了一个厉害的竞争对手-Google的Chrome浏览器。

图片 10

Chrome浏览器自2008年发布以来,就已经成为了很多用户的首选浏览器。这是之前属于Firefox的荣耀,但是随着时间的推移,由于Firefox的运行速度和极度臃肿,Chrome
在某些意义上已经代替了 Firefox。

火狐量子的发布就是为了扭转这种局势。它最大的优势就是它的速度,火狐声称它的运行速度是一些浏览器的两倍。

我们决定在配备Intel Core i5
2500k处理器和8GB内存台式电脑上测试一下,并在MacBook
Air上记录了我们的测试视频。

每个 Web
浏览器都使用默认设置进行测试,没有扩展或附件。这两个浏览器都没有启用广告拦截器也没有下载任何功能。浏览记录,缓存和Cookie也事先被清除。每个测试进行三次。

1. 抽奖项目的高频操作DOM问题

现已正式可用

到2017年6月,几乎所有浏览器都可以使用 async 和
await。为了确保你的代码随时可用,则需要使用 Babel 将你的 JavaScript
代码编译为旧浏览器也支持的语法。

如果对更多ES2017内容感兴趣,请访问ES2017特性的完整列表。

1 赞 收藏
评论

图片 5

JetStream 测试

JetStream
1.1测试浏览器运行高级Web应用程序的能力。它可以测试包括3D立方体旋转,整数数学运算和库解析。点击这里查看完整的列表。

得分越高的浏览器越好。

图片 12

图片 13

这一次,Firefox超过Chrome,但不是太多。这应证了 JetStream
的说法,它更适合“高级工作负载和编程技术”。

获胜者:Firefox Quantum

1.2 问题分析

下图为抽奖时文字滚动过程中的timeline记录。
图片 14

timeline分析:

  1. FPS:最上面一栏为绿色柱形为帧率(FPS),顶点值为60fps,上方红色方块表示长帧,这些长帧被Chrome称为jank(卡顿)。
  2. CPU:第二栏为CPU,蓝色表示loading(网络通信和HTML解析),黄色表示scripting(js执行时间),紫色表示rendering(样式计算和布局,即重排),
    绿色为painting(即重绘)。

更多timeline使用方法可参考:如何使用Chrome Timeline
工具(译)

由上图可以看出,在文字滚动过程中红色方块出现频繁,页面中存在的卡顿过多。帧率的值越低,人眼感受到的效果越差。
参考文章:脑洞大开:为啥帧率达到 60 fps
就流畅?。


接下来选择一段长帧区域放大来看
图片 15

在这段区域内最大一帧达到了49.7ms,帧率只有20fps,接下来看看这一帧里是什么因素耗时过长


图片 16

由上图可以看出,耗时最大的在scripting,js的执行时间达到了44.9ms,占总时间的93.2%,因为主要靠js计算控制DOM的显示内容,所以js运行时间过长。


选取一段FPS值很低的部分查看造成这段值低的原因
图片 17

由下图可看出主要为dom.html中的js执行占用时间。
图片 18

点进dom.html文件,即可定位到该函数
图片 19

由此可知,主要是rolling这个函数执行时间过长,对该部分失帧影响较大。而这个函数的主要作用就是实现文字的滚动效果,也可以从代码中看出,这个函数利用的setTimeout来反复执行,并且在这个函数中存在着循环以及大量的DOM操作,造成了页面的失帧等问题。

最佳方式:async / await

若干年前,async 函数纳入了 JavaScript 生态系统。就在上个月,async
函数成为了 JavaScript 语言的官方特性,并得到了广泛支持。

async 和 await 是建立在 Promise 和 generator上。本质上,允许我们使用
await 这个关键词在任何函数中的任何我们想要的地方进行暂停。

async function logger() { // pause until fetch returns let data = await
fetch(”) console.log(data) }

1
2
3
4
5
async function logger() {
    // pause until fetch returns
    let data = await fetch(‘http://sampleapi.com/posts’)
    console.log(data)
}

上面这段代码运行之后,得到了想要的结果。代码从 API 调用中记录了数据。

这种方式的好处就是非常直观。编写代码的方式就是大脑思考的方式,告诉脚本在需要的地方暂停。

另一个好处是,当我们不能使用 promise 时,还可以使用 try 和 catch:

async function logger () { try { let user_id = await
fetch(‘/api/users/username’) let posts = await
fetch(‘/api/`${user_id}`’) let object =
JSON.parse(user.posts.toString()) console.log(posts) } catch (error) {
console.error(‘Error:’, error) } }

1
2
3
4
5
6
7
8
9
10
async function logger ()  {
    try {
        let user_id = await fetch(‘/api/users/username’)
        let posts = await fetch(‘/api/`${user_id}`’)
        let object = JSON.parse(user.posts.toString())
        console.log(posts)
    } catch (error) {
        console.error(‘Error:’, error)
    }
}

上面是一个刻意写错的示例,为了证明了一点:在运行过程中,catch
可以捕获任何步骤中发生的错误。至少有三个地方,try
可能会失败,这是在异步代码中的一种最干净的方式来处理错误。

我们还可以使用带有循环和条件的 async 函数:

async function count() { let counter = 1 for (let i = 0; i ) { counter
+= 1 console.log(counter) await sleep(1000) } }

1
2
3
4
5
6
7
8
async function count() {
    let counter = 1
    for (let i = 0; i ) {
        counter += 1
        console.log(counter)
        await sleep(1000)
    }
}

这是一个很简答的例子,如果运行这段程序,将会看到代码在 sleep
调用时暂停,下一个循环迭代将会在1秒后启动。

2.2 优化方案

针对该项目中的问题,采取的解决方法是:

  • 尽量控制DOM的显示或隐藏,而不是删除或添加:

    页面加载时根据当前页面中吸顶导航的数量复制对应的DOM,并且隐藏这些导航。当页面滚动到指定区域后,显示对应的导航。

  • 一次性操作DOM:

    将复制的DOM存储到数组中,将该数组append到对应的父节点下,而不是根据复制得到DOM的数量依次循环插入到父节点下。

  • 多做缓存:

    如果某个节点将在后续进行多次操作,可以将该节点利用变量存储起来,而不是每次进行操作时都去查找一遍该节点。

  • 使用 requestAnimationFrame优化页面滚动

JavaScript

// 在页面滚动时对显示范围进行计算 //
延迟到整个dom加载完后再调用,并且异步到所有事件后执行 $(function(){
//animationShow优化滚动效果,scrollShow为实际计算显示范围及操作DOM的函数
setTimeout( function() { window.Scroller.on('scrollend',
animationShow); window.Scroller.on('scrollmove', animationShow); })
}); function animationShow(){ return window.requestAnimationFrame
?window.requestAnimationFrame(scrollShow) : scrollShow(); }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f6a8a92040460942071-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6a8a92040460942071-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f6a8a92040460942071-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6a8a92040460942071-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f6a8a92040460942071-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6a8a92040460942071-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f6a8a92040460942071-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6a8a92040460942071-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f6a8a92040460942071-9">
9
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6a8a92040460942071-10">
10
</div>
<div class="crayon-num" data-line="crayon-5b8f6a8a92040460942071-11">
11
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6a8a92040460942071-12">
12
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f6a8a92040460942071-1" class="crayon-line">
 // 在页面滚动时对显示范围进行计算
</div>
<div id="crayon-5b8f6a8a92040460942071-2" class="crayon-line crayon-striped-line">
  // 延迟到整个dom加载完后再调用,并且异步到所有事件后执行
</div>
<div id="crayon-5b8f6a8a92040460942071-3" class="crayon-line">
  $(function(){
</div>
<div id="crayon-5b8f6a8a92040460942071-4" class="crayon-line crayon-striped-line">
  //animationShow优化滚动效果,scrollShow为实际计算显示范围及操作DOM的函数
</div>
<div id="crayon-5b8f6a8a92040460942071-5" class="crayon-line">
      setTimeout( function() {
</div>
<div id="crayon-5b8f6a8a92040460942071-6" class="crayon-line crayon-striped-line">
          window.Scroller.on('scrollend', animationShow);
</div>
<div id="crayon-5b8f6a8a92040460942071-7" class="crayon-line">
          window.Scroller.on('scrollmove', animationShow);
</div>
<div id="crayon-5b8f6a8a92040460942071-8" class="crayon-line crayon-striped-line">
      })
</div>
<div id="crayon-5b8f6a8a92040460942071-9" class="crayon-line">
  });
</div>
<div id="crayon-5b8f6a8a92040460942071-10" class="crayon-line crayon-striped-line">
  function animationShow(){
</div>
<div id="crayon-5b8f6a8a92040460942071-11" class="crayon-line">
      return window.requestAnimationFrame ?window.requestAnimationFrame(scrollShow) : scrollShow();
</div>
<div id="crayon-5b8f6a8a92040460942071-12" class="crayon-line crayon-striped-line">
  }
</div>
</div></td>
</tr>
</tbody>
</table>

对于scroll的滚动优化还可以采用防抖(Debouncing)和节流(Throttling)的方式,但是防抖和节流的方式还是要借助于setTimeout,因此和requestAnimationFrame相比,还是requestAnimationFrame实现效果好一些。
参考文章:高性能滚动 scroll
及页面渲染优化

发表评论

电子邮件地址不会被公开。 必填项已用*标注