提高 HTML5 应用程序的性能
HTML5 为我们提供了强大的工具来增强 Web 应用程序的视觉外观。 在动画领域尤其如此。 然而,伴随着这种新的力量也带来了新的挑战。 事实上,这些挑战并不是什么新鲜事,有时问问你友好的同桌 Flash 程序员,她过去是如何克服类似问题的,这可能是有意义的。
无论如何,当您从事动画工作时,用户认为这些动画是否流畅变得非常重要。 我们需要认识到的是,动画中的流畅度并不能通过简单地将每秒帧数增加到超过任何认知阈值来真正创建。 不幸的是,我们的大脑比这更聪明。 您将了解到,真正的每秒 30 帧动画 (fps) 比 60 fps 好得多,中间只掉了几帧。 人们讨厌锯齿状。
本文将尝试为您提供改善您自己的应用程序体验的工具和技术。
策略
我们决不想阻止您使用 HTML5 构建出色、令人惊叹的视觉应用程序。
然后,当您注意到性能可能会好一点时,请返回此处并阅读有关如何改进应用程序元素的信息。 当然,它可以帮助您从一开始就把一些事情做对,但永远不要让它妨碍您提高工作效率。
HTML5 的视觉保真度++
硬件加速
硬件加速是浏览器整体渲染性能的一个重要里程碑。 一般方案是将原本由主 CPU 计算的任务卸载到计算机图形适配器中的图形处理单元 (GPU)。 这可以产生巨大的性能提升,还可以减少移动设备上的资源消耗。
GPU 可以加速文档的这些方面:
- 通用布局合成
- CSS3 过渡
- CSS3 3D 变换
- 画布绘图
- WebGL 3D绘图
虽然画布和 WebGL 的加速是特殊用途的功能,可能不适用于您的特定应用程序,但前三个方面可以帮助几乎每个应用程序变得更快。
什么可以加速?
GPU 加速的工作原理是将明确定义的特定任务卸载到专用硬件。 一般方案是将您的文档分解为多个“层”,这些层对于加速的页面方面是不变的。 这些层是使用传统渲染管线渲染的。 然后使用 GPU 将图层合成到单个页面上,应用可以动态加速的“效果”。 一个可能的结果是在屏幕上动画的对象在动画发生时不需要页面的单个“重新布局”。
您需要摆脱的是,您需要让渲染引擎轻松识别何时可以应用它的 GPU 加速魔法。
考虑以下示例:
您想要使浏览器中的元素从左到右动画化。 传统的做法是设置一个 JavaScript 计时器,然后每隔 N 毫秒重复设置样式对象的“left”属性。
虽然这有效,但浏览器并不真正知道您正在执行一些应该被人类视为流畅动画的事情。 考虑一下当您使用 CSS3 过渡实现相同的视觉外观时会发生什么:
现在,您只需定义对象的最终位置,然后告诉浏览器在特定时间段内执行到达最终目的地的动画。
浏览器如何实现这个动画对开发者是完全隐藏的。 这反过来意味着浏览器能够应用 GPU 加速等技巧来实现定义的目标。
Chrome 有两个有用的命令行标志来帮助调试 GPU 加速:
--show-composited-layer-borders
在 GPU 级别操作的元素周围显示红色边框。 有助于确认您的操作发生在 GPU 层内。--show-paint-rects
所有非 GPU 更改都被绘制,这会在所有重新绘制的区域周围抛出一个浅色边框。 您可以看到浏览器正在优化绘制区域。
Safari 具有类似的运行时标志, 描述如下 。
CSS3 过渡
CSS Transitions 使样式动画对每个人来说都是微不足道的,但它们也是一种智能性能特性。 因为 CSS 过渡是由浏览器管理的,所以它的动画保真度可以大大提高,并且在很多情况下硬件加速。 目前,WebKit(Chrome、Safari、iOS)具有硬件加速的 CSS 转换,但它很快就会用于其他浏览器和平台。
您可以使用 “transitionEnd” 事件将其编写成强大的组合,但现在捕获所有支持的过渡结束事件意味着观看 "webkitTransitionEnd transitionend oTransitionEnd"
.
许多库现在引入了动画 API,如果存在则利用过渡,否则回退到标准 DOM 样式动画。 scripty2 、 YUI 转换 、 jQuery 动画增强 。
CSS3 翻译
我确定您之前已经发现自己在页面上设置了元素的 x/y 位置的动画。 您可能操纵了内联样式的 left 和 top 属性。 通过 2D 变换,我们可以使用 translate()
复制此行为的功能。
我们可以将它与 DOM 动画结合起来使用最好的东西
<div style="position:relative; height:120px;" class="hwaccel">
<div style="padding:5px; width:100px; height:100px; background:papayaWhip;
position:absolute;">
</div>
</div>
<script>
document.querySelector('#box').addEventListener('click', moveIt, false);
function moveIt(evt) {
var elem = evt.target;
if (Modernizr.csstransforms && Modernizr.csstransitions) {
// vendor prefixes omitted here for brevity
elem.style.transition = 'all 3s ease-out';
elem.style.transform = 'translateX(600px)';
} else {
// if an older browser, fall back to jQuery animate
jQuery(elem).animate({ 'left': '600px'}, 3000);
}
}
</script>
点我让我向右移动
我们使用 Modernizr 来测试 CSS 2D Transforms 和 CSS Transitions,如果是这样,我们将使用 translate 来移动位置。 如果这是使用过渡动画,那么浏览器很有可能可以对其进行硬件加速。 为了让浏览器再次朝着正确的方向前进,我们将使用上面的“magic CSS bullet”。
如果我们的浏览器功能较弱,我们将回退到 jQuery 来移动我们的元素。 您可以选择 Louis-Rémi Babé 的jQuery Transform polyfill 插件 来使这一切自动化。
window.requestAnimationFrame
requestAnimationFrame
由 Mozilla 引入并由 WebKit 迭代,目的是为您提供一个用于运行动画的本机 API,无论它们是基于 DOM/CSS 还是基于 <canvas>
或 WebGL。 浏览器可以将并发动画优化为单个回流和重绘循环,从而产生更高保真度的动画。 例如,基于 JS 的动画与 CSS 转换或 SVG SMIL 同步。 此外, 如果您在不可见的选项卡中运行动画循环,浏览器将不会使其继续运行 ,这意味着更少的 CPU、GPU 和内存使用,从而延长电池寿命。
有关如何以及为何使用的更多详细信息 requestAnimationFrame
,查看 Paul Irish 的文章 requestAnimationFrame for smart animating 。
剖析
当您发现您的应用程序的速度可以提高时,是时候深入分析以找出优化可以产生最大收益的地方了。 优化通常会对源代码的可维护性产生负面影响,因此只应在必要时应用。 分析会告诉您代码的哪些部分在提高性能时会产生最大的好处。
JavaScript 分析
JavaScript 分析器通过测量每个函数从开始到结束执行所花费的时间,让您在 JavaScript 函数级别概览应用程序的性能。
一个函数的总执行时间是它从上到下执行它所花费的总时间。 净执行时间是总执行时间减去执行从函数调用的函数所花费的时间。
有些函数比其他函数更频繁地被调用。 探查器通常会为您提供所有调用运行所花费的时间以及平均、最小和最大执行时间。
有关更多详细信息,请查看 有关分析的 Chrome 开发工具文档 。
DOM
JavaScript 的性能对您的应用程序的流畅性和响应性有很大的影响。 理解这一点很重要,虽然 JavaScript 分析器测量 JavaScript 的执行时间,但它们也间接测量花在 DOM 操作上的时间。 这些 DOM 操作通常是性能问题的核心。
function drawArray(array) {
for(var i = 0; i < array.length; i++) {
document.getElementById('test').innerHTML += array[i]; // No good :(
}
}
例如,在上面的代码中,几乎没有时间花在执行实际的 JavaScript 上。 drawArray 函数仍然很可能会出现在您的配置文件中,因为它以非常浪费的方式与 DOM 交互。
技巧和窍门
匿名函数
匿名函数不容易分析,因为它们天生就没有可以在分析器中显示的名称。 有两种方法可以解决此问题:
$('.stuff').each(function() { ... });
重写为:
$('.stuff').each(function workOnStuff() { ... });
众所周知,JavaScript 支持命名函数表达式。 这样做将使它们完美地显示在分析器中。 该解决方案存在一个问题:命名表达式实际上将函数名称放入了当前词法范围。 这可能会破坏其他符号,所以要小心。
分析长函数
假设您有一个很长的函数,并且您怀疑它的一小部分可能是导致性能问题的原因。 有两种方法可以找出问题出在哪个部分:
- 正确的方法:重构您的代码以不包含任何长函数。
- 邪恶的完成方法:以命名自调用函数的形式向您的代码添加语句。 如果你小心一点,这不会改变语义,它会使你的函数的一部分在分析器中显示为单独的函数:
不要忘记在分析完成后删除这些额外的功能; 甚至将它们用作重构代码的起点。function myLongFunction() { ... (function doAPartOfTheWork() { ... })(); ... }
DOM 分析
最新的 Chrome Web Inspector 开发工具包含新的“时间线视图”,它显示浏览器执行的低级操作的时间线。 您可以使用此信息来优化您的 DOM 操作。 您的目标应该是减少浏览器在代码执行时必须执行的“操作”数量。
时间线视图可以创建大量信息。 因此,您应该尝试创建可以独立执行的最少测试用例。
上图显示了一个非常简单的脚本的时间轴视图的输出。 左窗格按时间顺序显示浏览器执行的操作,而右窗格中的时间线显示单个操作实际消耗的时间。
有关时间轴视图的更多信息。 在 Internet Explorer 中进行概要分析的替代工具是 DynaTrace Ajax Edition 。
剖析策略
挑出方面
当您想要分析您的应用程序时,请尝试尽可能挑出其功能中可能触发缓慢的方面。 然后尝试执行配置文件运行,只执行与应用程序的这些方面相关的部分代码。 这将使分析数据更易于解释,因为它不会与与您的实际问题无关的代码路径混合在一起。 应用程序各个方面的好例子可能是:
- 启动时间(激活分析器,重新加载应用程序,等待初始化完成,停止分析器。
- 单击一个按钮和后续动画(启动分析器,单击按钮,等待动画完成,停止分析器)。
GUI Profiling
在 GUI 程序中只执行应用程序的正确部分比优化 3D 引擎的光线追踪器更难。 例如,当您想要分析单击按钮时发生的事情时,您可能会触发不相关的鼠标悬停事件,从而使您的结果不那么确定。 尽量避免这种情况 :)
编程接口
还有一个用于激活调试器的编程接口。 这允许精确控制分析何时开始和何时结束。
开始分析:
console.profile()
停止分析:
console.profileEnd()
重复性
当您进行分析时,请确保您可以实际重现您的结果。 只有这样,您才能判断您的优化是否确实改善了事情。 功能级别的分析也是在整个计算机的上下文中完成的。 这不是一门精确的科学。 个人配置文件运行可能会受到计算机上发生的许多其他事情的影响:
- 您自己的应用程序中的一个不相关的计时器在您测量其他内容时触发。
- 垃圾收集器正在工作。
- 浏览器中的另一个选项卡在同一个操作线程中努力工作。
- 您计算机上的另一个程序占用了 CPU,从而使您的应用程序变慢。
- 地球引力场的突然变化。
在一个分析会话中多次执行相同的代码路径也很有意义。 通过这种方式,您可以减少上述因素的影响,并且缓慢的部分可能会更加明显。
测量、改进、测量
当您发现程序中的慢点时,请尝试想办法改进执行行为。 更改代码后,再次配置文件。 如果您对结果感到满意,请继续,如果您没有看到改进,您可能应该回滚您的更改并且不要将其留在“因为它不会造成伤害”。
优化策略
最小化 DOM 交互
提高 Web 客户端应用程序速度的一个常见主题是尽量减少 DOM 交互。 虽然 JavaScript 引擎的速度提高了一个数量级,但访问 DOM 的速度并没有以同样的速度提高。 这也是出于永远不会发生的非常实际的原因(比如在屏幕上布局和绘制东西只是需要时间)。
缓存 DOM 节点
每当您从 DOM 中检索一个节点或节点列表时,请尝试考虑您是否能够在以后的计算中重用它们(甚至只是下一个循环迭代)。 只要不在相关区域实际添加或删除节点,通常就是这种情况。
前:
function getElements() {
return $('.my-class');
}
后:
var cachedElements;
function getElements() {
if (cachedElements) {
return cachedElements;
}
cachedElements = $('.my-class');
return cachedElements;
}
缓存属性值
与缓存 DOM 节点的方式相同,您也可以缓存属性值。 假设您正在为节点样式的属性设置动画。 如果您知道您(如代码的那部分)是唯一会触及该属性的人,您可以在每次迭代时缓存最后一个值,这样您就不必重复读取它。
前:
setInterval(function() {
var ele = $('#element');
var left = parseInt(ele.css('left'), 10);
ele.css('left', (left + 5) + 'px');
}, 1000 / 30);
后:
var ele = $('#element');
var left = parseInt(ele.css('left'), 10);
setInterval(function() {
left += 5;
ele.css('left', left + 'px');
}, 1000 / 30);
将 DOM 操作移出循环
循环通常是优化的热点。 尝试想办法将实际的数字运算与使用 DOM 分离。 通常可以进行计算,然后在计算完成后一次性应用所有结果。
前:
document.getElementById('target').innerHTML = '';
for(var i = 0; i < array.length; i++) {
var val = doSomething(array[i]);
document.getElementById('target').innerHTML += val;
}
后:
var stringBuilder = [];
for(var i = 0; i < array.length; i++) {
var val = doSomething(array[i]);
stringBuilder.push(val);
}
document.getElementById('target').innerHTML = stringBuilder.join('');
重绘和回流
如前所述,访问 DOM 相对较慢。 当您的代码读取必须重新计算的值时,它会变得非常慢,因为您的代码最近修改了 DOM 中的相关内容。 因此,应该避免混合对 DOM 的读写访问。 理想情况下,您的代码应始终分为两个阶段:
- 第 1 阶段:读取代码所需的 DOM 值
- 阶段 2:修改 DOM
尽量不要编写如下模式:
- 阶段 1:读取 DOM 值
- 阶段 2:修改 DOM
- 第三阶段:多读一些
- 阶段 4:在其他地方修改 DOM。
前:
function paintSlow() {
var left1 = $('#thing1').css('left');
$('#otherThing1').css('left', left);
var left2 = $('#thing2').css('left');
$('#otherThing2').css('left', left);
}
后:
function paintFast() {
var left1 = $('#thing1').css('left');
var left2 = $('#thing2').css('left');
$('#otherThing1').css('left', left);
$('#otherThing2').css('left', left);
}
对于在一个 JavaScript 执行上下文中发生的操作,应考虑此建议。 (例如,在事件处理程序中、间隔处理程序中或处理 ajax 响应时。)
从上面执行函数 paintSlow() 创建这个图像:
切换到更快的实现会产生此图像:
这些图像表明,重新排序代码访问 DOM 的方式可以极大地提高渲染性能。 在这种情况下,原始代码必须重新计算样式和页面布局两次才能创建相同的结果。 类似的优化可以应用于基本上所有“真实世界”的代码,并产生一些非常显着的结果。
阅读更多: 重绘、回流/重新布局、重新设计样式 渲染: Stoyan Stefanov 的
重绘和事件循环
浏览器中的 JavaScript 执行遵循“事件循环”模型。 默认情况下,浏览器处于“空闲”状态。 这种状态可以被来自用户交互的事件或诸如 JavaScript 计时器或 Ajax 回调之类的事件打断。 每当一段 JavaScript 在这样的中断点运行时,浏览器通常会等待它完成,直到它重新绘制屏幕(对于运行时间极长的 JavaScript 或在有效中断 JavaScript 执行的警告框等情况下可能会有例外).
结果
- 如果您的 JavaScript 动画周期执行时间超过 1/30 秒,您将无法创建流畅的动画,因为浏览器不会在 JS 执行期间重新绘制。 当您希望还处理用户事件时,您需要更快。
- 有时将某些 JavaScript 操作延迟到稍后会派上用场。
例如setTimeout(function() { ... }, 0)
这有效地告诉浏览器在事件循环再次空闲时立即执行回调(实际上某些浏览器将等待至少 10 毫秒)。 您需要注意,这将创建两个在时间上非常接近的 JavaScript 执行周期。 两者都可能触发屏幕重绘,这可能会使绘画花费的总时间加倍。 这是否真的会触发两次绘制取决于浏览器中的试探法。
普通版:
function paintFast() {
var height1 = $('#thing1').css('height');
var height2 = $('#thing2').css('height');
$('#otherThing1').css('height', '20px');
$('#otherThing2').css('height', '20px');
}
让我们添加一些延迟:
function paintALittleLater() {
var height1 = $('#thing1').css('height');
var height2 = $('#thing2').css('height');
$('#otherThing1').css('height', '20px');
setTimeout(function() {
$('#otherThing2').css('height', '20px');
}, 10)
}
延迟版本显示浏览器绘制了两次,尽管对页面的两次更改仅为 1/100 秒。
惰性初始化
用户想要加载速度快且反应灵敏的 Web 应用程序。 然而,用户对他们认为慢的东西有不同的阈值,这取决于他们所做的动作。 例如,应用程序不应该对鼠标悬停事件进行大量计算,因为这可能会在用户继续移动鼠标时造成糟糕的用户体验。 然而,用户习惯于在点击按钮后接受一点延迟。
因此,将您的初始化代码移动到尽可能晚执行(例如,当用户单击激活应用程序特定组件的按钮时)可能是有意义的。
前:
var things = $('.ele > .other * div.className');
$('#button').click(function() { things.show() });
后:
$('#button').click(function() { $('.ele > .other * div.className').show() });
事件委托
在页面上传播事件处理程序可能需要相对较长的时间,并且一旦元素被动态替换也可能很乏味,然后需要将事件处理程序重新附加到新元素。
这种情况下的解决方案是使用一种称为事件委托的技术。 不是将单独的事件处理程序附加到元素,而是使用许多浏览器事件的冒泡特性,将事件处理程序实际附加到父节点并检查事件的目标节点以查看是否对事件感兴趣。
在 jQuery 中,这可以很容易地表示为:
$('#parentNode').delegate('.button', 'click', function() { ... });
何时不使用事件委托:
有时情况可能恰恰相反:您正在使用事件委托,但遇到了性能问题。 基本上,事件委托允许复杂度恒定的初始化时间。 但是,必须为该事件的每次调用支付检查事件是否感兴趣的代价。 这可能代价高昂,尤其是对于频繁发生的事件,如“鼠标悬停”或什至“鼠标移动”。
典型问题及解决方案
我做的事情 $(document).ready
花费很长时间
Malte 的个人建议:永远不要在 $(document).ready
. 尝试以最终形式交付文档。 好的,您可以注册事件侦听器,但只能使用 id-selector 和/或使用事件委托。 对于诸如“mousemove”之类的昂贵事件,延迟注册直到需要它们(相关元素上的鼠标悬停事件)。
如果你真的需要做一些事情,比如发出一个 Ajax 请求来获取实际数据,那么就显示一个漂亮的动画; 如果动画是动画 GIF 等,您可能希望将动画作为数据 URI 包含在内。
因为我在页面上添加了一个 Flash 电影,所以一切都很慢
将 Flash 添加到页面总是会稍微减慢渲染速度,因为窗口的最终布局必须在浏览器和 Flash 插件之间“协商”。 当您无法完全避免在页面上放置 Flash 时,请确保将“wmode”Flash 参数设置为值“window”(默认值)。 这将禁用组合 HTML 和 Flash 元素的能力(您将无法看到位于 Flash 影片之上的 HTML 元素,并且您的 Flash 影片不能是透明的)。 这可能会带来不便,但会显着提高您的表现。 方式。 例如,查看youtube.com 小心避免在主要电影播放器上方放置图层的
我正在将内容保存到 localStorage,现在我的应用程序卡顿了
写入 localStorage 是同步操作,涉及启动硬盘。 你永远不想在做动画时做“长时间运行”的同步操作。 将对 localStorage 的访问权限移动到代码中您确定用户空闲且没有动画进行的位置。
分析指向一个 jQuery 选择器真的很慢
首先,您要确保您的选择器可以通过 document.querySelectorAll 运行。 您可以在 JavaScript 控制台中对其进行测试。 如果出现异常,请重写您的选择器以不使用您的 JavaScript 框架的任何特殊扩展。 这将使现代浏览器中的选择器加速一个数量级。
如果这没有帮助,或者如果您还想在现代浏览器中更快,请遵循以下准则:
- 在选择器的右侧尽可能具体。
- 使用您不经常使用的标签名称作为最右边的选择器部分。
- 如果没有任何帮助,请考虑重写一些东西,以便您可以使用 id-selector
所有这些 DOM 操作都需要很长时间
一堆 DOM 节点插入、删除和更新可能非常慢。 这通常可以通过生成大量 html 字符串并使用 domNode.innerHTML = newHTML
替换旧内容。 请注意,这可能对可维护性非常不利,并且可能会在 IE 中创建内存链接,因此请小心。
另一个常见问题是您的初始化代码可能会创建大量 HTML。 例如,一个将选择框转换为一堆 div 的 jQuery 插件,因为这是设计人员在不了解 UX 最佳实践的情况下想要的。 如果你真的想让你的页面变快,永远不要那样做。 而是以最终形式从服务器端交付所有标记。 这又存在很多问题,因此请仔细考虑速度是否值得权衡。
工具
- JSPerf - 对 JavaScript 的小片段进行基准测试
- Firebug - 用于在 Firefox 中进行分析
- Google Chrome 开发者工具 (在 Safari 中作为 WebInspector 提供)
- DOM Monster - 用于优化 DOM 性能
- DynaTrace Ajax Edition - 用于在 Internet Explorer 中进行分析和绘制优化
延伸阅读
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Canvas 画布的特效制作
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论