xml地图|网站地图|网站标签 [设为首页] [加入收藏]
启动性能瓶颈分析与解决方案,最佳实践
分类:web前端

Canvas 最佳实践(性能篇)

2016/02/23 · HTML5 · Canvas

原文出处: 淘宝前端团队(FED)- 叶斋   

图片 1

Canvas 想必前端同学们都不陌生,它是 HTML5 新增的「画布」元素,允许我们使用 JavaScript 来绘制图形。目前,所有的主流浏览器都支持 Canvas。

图片 2

Canvas 最常见的用途是渲染动画。渲染动画的基本原理,无非是反复地擦除和重绘。为了动画的流畅,留给我渲染一帧的时间,只有短短的 16ms。在这 16ms 中,我不仅需要处理一些游戏逻辑,计算每个对象的位置、状态,还需要把它们都画出来。如果消耗的时间稍稍多了一些,用户就会感受到「卡顿」。所以,在编写动画(和游戏)的时候,我无时无刻不担忧着动画的性能,唯恐对某个 API 的调用过于频繁,导致渲染的耗时延长。

为此,我做了一些实验,查阅了一些资料,整理了平时使用 Canvas 的若干心得体会,总结出这一片所谓的「最佳实践」。如果你和我有类似的困扰,希望本文对你有一些价值。

本文仅讨论 Canvas 2D 相关问题。

JavaScript 启动性能瓶颈分析与解决方案

2017/02/17 · JavaScript · 性能

原文出处: Addy Osmani   译文出处:王下邀月熊_Chevalier   

图片 3

在 Web 开发中,随着需求的增加与代码库的扩张,我们最终发布的 Web 页面也逐渐膨胀。不过这种膨胀远不止意味着占据更多的传输带宽,其还意味着用户浏览网页时可能更差劲的性能体验。浏览器在下载完某个页面依赖的脚本之后,其还需要经过语法分析、解释与运行这些步骤。而本文则会深入分析浏览器对于 JavaScript 的这些处理流程,挖掘出那些影响你应用启动时间的罪魁祸首,并且根据我个人的经验提出相对应的解决方案。回顾过去,我们还没有专门地考虑过如何去优化 JavaScript 解析/编译这些步骤;我们预想中的是解析器在发现<script>标签后会瞬时完成解析操作,不过这很明显是痴人说梦。下图是对于 V8 引擎工作原理的概述:
图片 4下面我们深入其中的关键步骤进行分析。

Web 的现状:网页性能提升指南

2017/09/21 · 基础技术 · 性能

原文出处: Karolina Szczur   译文出处:碧青_Kwok   

互联网发展非常迅速,所以我们创造了Web平台。通常我们会忽视连通性等问题,但用户们却不会视而不见。一瞥万维网的现状,可以发现我们并没有用同情心、变通意识去构建它,更不要说性能了。

所以,今天的Web是什么状态呢?

在这个星球上的74亿人中,只有46%可以上网。平均网络速度上限为7Mb/s。更重要的是,有93%的互联网用户正在通过移动设备进行访问——若不适配移动设备将引起用户反感。通常情况下,数据比我们假设的更昂贵——可能需要1到13小时才能购买500MB的数据包(德国 vs. 巴西;更有趣的统计数据参见 Ben Schwarz 的 Beyond the Bubble: The Real World Performance)。

我们的网站也不是完美的——平均网站是原始Doom游戏的大小(约3 MB)(请注意,为了统计准确,应使用中位数,阅读 Ilya Grigorik 的优秀“平均页面”是一个神话,中档网站大小目前为1.4MB)。图像可以轻松占用1.7 MB的带宽,而JavaScript平均值也有400KB的体积。这不仅是Web平台的问题,原生应用程序可能更糟,还记得为了获取错误修复版本,而下载200MB安装包的情景吗?

技术人员经常会发现自己处于特权状态。随着最新的高端笔记本电脑、手机和快速有线互联网连接,很容易让我们忘记,这些并不是每个人都有的条件(实际上,真的很少)。

如果我们从特权和缺乏同情的角度来构建网络平台,那么将导致排他性的糟糕体验。

考虑到设计和开发的性能,我们怎样才能做得更好?


计算与渲染

把动画的一帧渲染出来,需要经过以下步骤:

  1. 计算:处理游戏逻辑,计算每个对象的状态,不涉及 DOM 操作(当然也包含对 Canvas 上下文的操作)。
  2. 渲染:真正把对象绘制出来。
    2.1. JavaScript 调用 DOM API(包括 Canvas API)以进行渲染。
    2.2. 浏览器(通常是另一个渲染线程)把渲染后的结果呈现在屏幕上的过程。

图片 5

之前曾说过,留给我们渲染每一帧的时间只有 16ms。然而,其实我们所做的只是上述的步骤中的 1 和 2.1,而步骤 2.2 则是浏览器在另一个线程(至少几乎所有现代浏览器是这样的)里完成的。动画流畅的真实前提是,以上所有工作都在 16ms 中完成,所以 JavaScript 层面消耗的时间最好控制在 10ms 以内。

虽然我们知道,通常情况下,渲染比计算的开销大很多(3~4 个量级)。除非我们用到了一些时间复杂度很高的算法(这一点在本文最后一节讨论),计算环节的优化没有必要深究。

我们需要深入研究的,是如何优化渲染的性能。而优化渲染性能的总体思路很简单,归纳为以下几点:

  1. 在每一帧中,尽可能减少调用渲染相关 API 的次数(通常是以计算的复杂化为代价的)。
  2. 在每一帧中,尽可能调用那些渲染开销较低的 API。
  3. 在每一帧中,尽可能以「导致渲染开销较低」的方式调用渲染相关 API。

到底是什么拖慢了我们应用的启动时间?

在启动阶段,语法分析,编译与脚本执行占据了 JavaScript 引擎运行的绝大部分时间。换言之,这些过程造成的延迟会真实地反应到用户可交互时延上;譬如用户已经看到了某个按钮,但是要好几秒之后才能真正地去点击操作,这一点会大大影响用户体验。
图片 6上图是我们使用 Chrome Canary 内置的 V8 RunTime Call Stats 对于某个网站的分析结果;需要注意的是桌面浏览器中语法解析与编译占用的时间还是蛮长的,而在移动端中占用的时间则更长。实际上,对于 Facebook, Wikipedia, Reddit 这些大型网站中语法解析与编译所占的时间也不容忽视:
图片 7上图中的粉色区域表示花费在 V8 与 Blink’s C++ 中的时间,而橙色和黄色分别表示语法解析与编译的时间占比。Facebook 的 Sebastian Markbage 与 Google 的 Rob Wormald 也都在 Twitter 发文表示过 JavaScript 的语法解析时间过长已经成为了不可忽视的问题,后者还表示这也是 Angular 启动时主要的消耗之一。
图片 8

随着移动端浪潮的涌来,我们不得不面对一个残酷的事实:移动端对于相同包体的解析与编译过程要花费相当于桌面浏览器2~5倍的时间。当然,对于高配的 iPhone 或者 Pixel 这样的手机相较于 Moto G4 这样的中配手机表现会好很多;这一点提醒我们在测试的时候不能仅用身边那些高配的手机,而应该中高低配兼顾:
图片 9

上图是部分桌面浏览器与移动端浏览器对于 1MB 的 JavaScript 包体进行解析的时间对比,显而易见的可以发现不同配置的移动端手机之间的巨大差异。当我们应用包体已经非常巨大的时候,使用一些现代的打包技巧,譬如代码分割,TreeShaking,Service Workder 缓存等等会对启动时间有很大的影响。另一个角度来看,即使是小模块,你代码写的很糟或者使用了很糟的依赖库都会导致你的主线程花费大量的时间在编译或者冗余的函数调用中。我们必须要清醒地认识到全面评测以挖掘出真正性能瓶颈的重要性。

优化所有资源

理解浏览器如何分析和处理资源,是显著提高性能的最强大但未充分利用的方式之一。事实证明,浏览器在嗅探资源方面非常出色,同时解析并确定其优先级。这里是关键请求的来源。

如果请求中包含用户视口中呈现内容所必需的资源,则该请求至关重要。

对于大多数网站,它将是HTML、必要的CSS、logo、网络字体,也可能是图片。在许多情况下,几十个其他不相关的资源(JavaScript、跟踪代码、广告等)影响了关键请求。幸运的是,我们能够通过仔细挑选重要资源并调整优先级来控制这种行为。

通过``我们可以手动强制调高资源的优先级,确保所需的内容按时呈现。这种技术可以显著改善“交互时间”指标,从而使最佳的用户体验成为可能。

图片 10

关键请求对许多人来说,似乎仍然是一个黑匣子,可共享资料的缺乏并不能改变现状。幸运的是,Ben Schwarz
发表了关于这个问题的非常全面并平易近人的文章——关键请求。另外,请参阅Addy的文章,在Chrome中的预加载、预取和优先级(Preload, Prefetch and Priorities in Chrome)。

图片 11

[在Chrome开发人员工具中启用优先级]

要跟踪在请求优先级处理方面的情况,请使用Lighthouse性能工具和关键请求链审核工具,或查看Chrome开发人员工具中“网络”选项卡下的请求优先级。

Canvas 上下文是状态机

Canvas API 都在其上下文对象 context 上调用。

JavaScript

var context = canvasElement.getContext('2d');

1
var context = canvasElement.getContext('2d');

我们需要知道的第一件事就是,context 是一个状态机。你可以改变 context 的若干状态,而几乎所有的渲染操作,最终的效果与 context 本身的状态有关系。比如,调用 strokeRect 绘制的矩形边框,边框宽度取决于 context 的状态 lineWidth,而后者是之前设置的。

JavaScript

context.lineWidth = 5; context.strokeColor = 'rgba(1, 0.5, 0.5, 1)'; context.strokeRect(100, 100, 80, 80);

1
2
3
4
context.lineWidth = 5;
context.strokeColor = 'rgba(1, 0.5, 0.5, 1)';
 
context.strokeRect(100, 100, 80, 80);

图片 12

说到这里,和性能貌似还扯不上什么关系。那我现在就要告诉你,对 context.lineWidth 赋值的开销远远大于对一个普通对象赋值的开销,你会作如何感想。

当然,这很容易理解。Canvas 上下文不是一个普通的对象,当你调用了 context.lineWidth = 5 时,浏览器会需要立刻地做一些事情,这样你下次调用诸如 strokestrokeRect 等 API 时,画出来的线就正好是 5 个像素宽了(不难想象,这也是一种优化,否则,这些事情就要等到下次 stroke 之前做,更加会影响性能)。

我尝试执行以下赋值操作 106 次,得到的结果是:对一个普通对象的属性赋值只消耗了 3ms,而对 context 的属性赋值则消耗了 40ms。值得注意的是,如果你赋的值是非法的,浏览器还需要一些额外时间来处理非法输入,正如第三/四种情形所示,消耗了 140ms 甚至更多。

JavaScript

somePlainObject.lineWidth = 5; // 3ms (10^6 times) context.lineWidth = 5; // 40ms context.lineWidth = 'Hello World!'; // 140ms context.lineWidth = {}; // 600ms

1
2
3
4
somePlainObject.lineWidth = 5;  // 3ms (10^6 times)
context.lineWidth = 5;  // 40ms
context.lineWidth = 'Hello World!'; // 140ms
context.lineWidth = {}; // 600ms

context 而言,对不同属性的赋值开销也是不同的。lineWidth 只是开销较小的一类。下面整理了为 context 的一些其他的属性赋值的开销,如下所示。

属性 开销 开销(非法赋值)
line[Width/Join/Cap] 40+ 100+
[fill/stroke]Style 100+ 200+
font 1000+ 1000+
text[Align/Baseline] 60+ 100+
shadow[Blur/OffsetX] 40+ 100+
shadowColor 280+ 400+

与真正的绘制操作相比,改变 context 状态的开销已经算比较小了,毕竟我们还没有真正开始绘制操作。我们需要了解,改变 context 的属性并非是完全无代价的。我们可以通过适当地安排调用绘图 API 的顺序,降低 context 状态改变的频率。

JavaScript 语法解析与编译是否成为了大部分网站的瓶颈?

我曾不止一次听到有人说,我又不是 Facebook,你说的 JavaScript 语法解析与编译到
底会对其他网站造成什么样的影响呢?对于这个问题我也很好奇,于是我花费了两个月的时间对于超过 6000 个网站进行分析;这些网站囊括了 React,Angular,Ember,Vue 这些流行的框架或者库。大部分的测试是基于 WebPageTest 进行的,因此你可以很方便地重现这些测试结果。光纤接入的桌面浏览器大概需要 8 秒的时间才能允许用户交互,而 3G 环境下的 Moto G4 大概需要 16 秒 才能允许用户交互。
图片 13大部分应用在桌面浏览器中会耗费约 4 秒的时间进行 JavaScript 启动阶段(语法解析、编译、执行)
图片 14

而在移动端浏览器中,大概要花费额外 36% 的时间来进行语法解析:
图片 15

另外,统计显示并不是所有的网站都甩给用户一个庞大的 JS 包体,用户下载的经过 Gzip 压缩的平均包体大小是 410KB,这一点与 HTTPArchive 之前发布的 420KB 的数据基本一致。不过最差劲的网站则是直接甩了 10MB 的脚本给用户,简直可怕。

图片 16

通过上面的统计我们可以发现,包体体积固然重要,但是其并非唯一因素,语法解析与编译的耗时也不一定随着包体体积的增长而线性增长。总体而言小的 JavaScript 包体是会加载地更快(忽略浏览器、设备与网络连接的差异),但是同样 200KB 的大小,不同开发者的包体在语法解析、编译上的时间却是天差地别,不可同日而语。

通用性能清单

  1. 积极地缓存
  2. 启用压缩
  3. 优化关键资源的优先级
  4. 使用CDN(Content Delivery Networks)

分层 Canvas

分层 Canvas 在几乎任何动画区域较大,动画较复杂的情形下都是非常有必要的。分层 Canvas 能够大大降低完全不必要的渲染性能开销。分层渲染的思想被广泛用于图形相关的领域:从古老的皮影戏、套色印刷术,到现代电影/游戏工业,虚拟现实领域,等等。而分层 Canvas 只是分层渲染思想在 Canvas 动画上最最基本的应用而已。

图片 17

分层 Canvas 的出发点是,动画中的每种元素(层),对渲染和动画的要求是不一样的。对很多游戏而言,主要角色变化的频率和幅度是很大的(他们通常都是走来走去,打打杀杀的),而背景变化的频率或幅度则相对较小(基本不变,或者缓慢变化,或者仅在某些时机变化)。很明显,我们需要很频繁地更新和重绘人物,但是对于背景,我们也许只需要绘制一次,也许只需要每隔 200ms 才重绘一次,绝对没有必要每 16ms 就重绘一次。

对于 Canvas 而言,能够在每层 Canvas 上保持不同的重绘频率已经是最大的好处了。然而,分层思想所解决的问题远不止如此。

使用上,分层 Canvas 也很简单。我们需要做的,仅仅是生成多个 Canvas 实例,把它们重叠放置,每个 Canvas 使用不同的 z-index 来定义堆叠的次序。然后仅在需要绘制该层的时候(也许是「永不」)进行重绘。

JavaScript

var contextBackground = canvasBackground.getContext('2d'); var contextForeground = canvasForeground.getContext('2d'); function render(){ drawForeground(contextForeground); if(needUpdateBackground){ drawBackground(contextBackground); } requestAnimationFrame(render); }

1
2
3
4
5
6
7
8
9
10
var contextBackground = canvasBackground.getContext('2d');
var contextForeground = canvasForeground.getContext('2d');
 
function render(){
  drawForeground(contextForeground);
  if(needUpdateBackground){
    drawBackground(contextBackground);
  }
  requestAnimationFrame(render);
}

记住,堆叠在上方的 Canvas 中的内容会覆盖住下方 Canvas 中的内容。

现代 JavaScript 语法解析 & 编译性能评测

图片优化

图片通常占网页传输的大部分有效载荷,因此图片优化可以带来最大的性能提升。有许多现有的策略和工具可以帮助我们删除额外的字节,但是首先应考虑的问题是:“图片对于我想传达的信息和效果至关重要吗?”。如果可以消除它,不仅可以节省带宽,而且还节省了请求。

在某些情况下,可以通过不同的技术实现类似的结果。比如CSS就具有艺术方向的一系列属性,例如阴影、渐变、动画及形状,允许我们构造适当风格的DOM元素。

绘制图像

目前,Canvas 中使用到最多的 API,非 drawImage 莫属了。(当然也有例外,你如果要用 Canvas 写图表,自然是半句也不会用到了)。

drawImage 方法的格式如下所示:

JavaScript

context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

1
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

图片 18

Chrome DevTools

打开 Timeline( Performance panel ) > Bottom-Up/Call Tree/Event Log 就会显示出当前网站在语法解析/编译上的时间占比。如果你希望得到更完整的信息,那么可以打开 V8 的 Runtime Call Stats。在 Canary 中,其位于 Timeline 的 Experims > V8 Runtime Call Stats 下。
图片 19

选择正确的格式

如果不能舍弃图片,确定哪种格式更合适就很重要了。首先要在矢量和光栅图形之间做出选择:

  • 矢量图形:分辨率独立,通常体积更小。非常适合logo、icon和简单的图形,包括基本形状(线,多边形,圆和点)。
  • 光栅图形:呈现更详细的信息,比较适合相片。

做出首个决定后,可以选择以下几种格式:JPEG、GIF、PNG–8、PNG–24,或最新的 WEBP 与 JPEG-XR 格式。有了这么多的选项,如何确保我们做出正确的选择?以下是找出最佳格式的基本方法:

  • JPEG:颜色非常丰富的图片(例如照片)
  • PNG–8:色彩相对单一的图片
  • PNG–24:局部透明的图片
  • GIF:动图

Photoshop可以通过各种设置,例如降低质量、降低噪音或色彩数量来优化以上每一种格式。确保设计师了解上述性能实践,并能够使用正确的方式优化相应格式的图片。如果您想了解更多如何处理图片,请阅读 Lara Hogan 的好文 Designing for Performance。

数据源与绘制的性能

由于我们具备「把图片中的某一部分绘制到 Canvas 上」的能力,所以很多时候,我们会把多个游戏对象放在一张图片里面,以减少请求数量。这通常被称为「精灵图」。然而,这实际上存在着一些潜在的性能问题。我发现,使用 drawImage 绘制同样大小的区域,数据源是一张和绘制区域尺寸相仿的图片的情形,比起数据源是一张较大图片(我们只是把数据扣下来了而已)的情形,前者的开销要小一些。可以认为,两者相差的开销正是「裁剪」这一个操作的开销。

我尝试绘制 104 次一块 320×180 的矩形区域,如果数据源是一张 320×180 的图片,花费了 40ms,而如果数据源是一张 800×800 图片中裁剪出来的 320×180 的区域,需要花费 70ms。

虽然看上去开销相差并不多,但是 drawImage 是最常用的 API 之一,我认为还是有必要进行优化的。优化的思路是,将「裁剪」这一步骤事先做好,保存起来,每一帧中仅绘制不裁剪。具体的,在「离屏绘制」一节中再详述。

Chrome Tracing

打开 about:tracing 页面,Chrome 提供的底层的追踪工具允许我们使用disabled-by-default-v8.runtime_stats来深度了解 V8 的时间消耗情况。V8 也提供了详细的指南来介绍如何使用这个功能。
图片 20

试用新格式

图像格式有几个较新的玩家,即WebP、JPEG 2000 和 JPEG-XR。它们都是由浏览器厂商开发的:Google 的 WebP,Apple 的 JPEG 2000 和 Microsoft 的 JPEG-XR。

WebP 是最受欢迎的竞争者,支持无损和有损压缩,这使得它非常灵活。无损的 WebP 比 PNG 小26%,比 JPG 小25-34%。WebP 有着74%的浏览器支持,它可以安全地进行降级,最多可节省1/3的传输字节。JPG 和 PNG 可以在 Photoshop 和其他图像处理应用程序以及命令行界面(brew install webp)中转换为WebP。

如果你想探索其他格式之间的视觉差异,推荐 Github 上这个很赞的 Demo。

视野之外的绘制

有时候,Canvas 只是游戏世界的一个「窗口」,如果我们在每一帧中,都把整个世界全部画出来,势必就会有很多东西画到 Canvas 外面去了,同样调用了绘制 API,但是并没有任何效果。我们知道,判断对象是否在 Canvas 中会有额外的计算开销(比如需要对游戏角色的全局模型矩阵求逆,以分解出对象的世界坐标,这并不是一笔特别廉价的开销),而且也会增加代码的复杂程度,所以关键是,是否值得。

我做了一个实验,绘制一张 320×180 的图片 104 次,当我每次都绘制在 Canvas 内部时,消耗了 40ms,而每次都绘制在 Canvas 外时,仅消耗了 8ms。大家可以掂量一下,考虑到计算的开销与绘制的开销相差 2~3 个数量级,我认为通过计算来过滤掉哪些画布外的对象,仍然是很有必要的。

WebPageTest

图片 21WebPageTest 中 Processing Breakdown 页面在我们启用 Chrome > Capture Dev Tools Timeline 时会自动记录 V8 编译、EvaluateScript 以及 FunctionCall 的时间。我们同样可以通过指明disabled-by-default-v8.runtime_stats的方式来启用 Runtime Call Stats。
图片 22

更多使用说明参考我的gist点击预览。

用工具和算法进行优化

即使使用了高效的图像格式,也不应跳过后处理优化。这一步很重要。

如果您选择了尺寸相对较小的 SVG,它们也是可以再次压缩的。SVGO 是一个命令行工具,可以通过剥离不必要的元数据来快速优化 SVG。另外,如果您喜欢Web界面或受操作系统的限制,请使用 Jake Archibald 的 SVGOMG。因为 SVG 是基于 XML 的格式,它也可以在服务器端进行 GZIP 压缩。

ImageOptim 是大多其他图像类型的最好选择。包括 pngcrush、pngquant、MozJPEG、Google Zopfli等,它在一个全面的开源包中捆绑了一大堆优秀的工具。ImageOptim 可以以 Mac OS 应用程序、命令行界面和 Sketch 插件形式,轻松地实现到现有的工作流程中。对于那些在 Linux 或 Windows 上的场景,大多数 ImageOptim 的 CLI 都可以在您的平台上使用。

如果您倾向于尝试新兴的编码器,Google 今年早些时候发布了 Guetzli——源自 WebP 和 Zopfli 的开源算法。Guetzli 可以比任何其他可用的压缩方法生成35%更小体积的 JPEG。唯一的缺点是:处理时间慢(CPU 每处理百万像素需要1分钟)。

选择工具时,请确保它们生成所需的结果并适应团队的工作流程。理想情况是,将优化过程自动化,这样就不会产生漏掉的情况。

离屏绘制

上一节提到,绘制同样的一块区域,如果数据源是尺寸相仿的一张图片,那么性能会比较好,而如果数据源是一张大图上的一部分,性能就会比较差,因为每一次绘制还包含了裁剪工作。也许,我们可以先把待绘制的区域裁剪好,保存起来,这样每次绘制时就能轻松很多。

drawImage 方法的第一个参数不仅可以接收 Image 对象,也可以接收另一个 Canvas 对象。而且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎完全一致。我们只需要实现将对象绘制在一个未插入页面的 Canvas 中,然后每一帧使用这个 Canvas 来绘制。

JavaScript

// 在离屏 canvas 上绘制 var canvasOffscreen = document.createElement('canvas'); canvasOffscreen.width = dw; canvasOffscreen.height = dh; canvasOffscreen.getContext('2d').drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); // 在绘制每一帧的时候,绘制这个图形 context.drawImage(canvasOffscreen, x, y);

1
2
3
4
5
6
7
8
// 在离屏 canvas 上绘制
var canvasOffscreen = document.createElement('canvas');
canvasOffscreen.width = dw;
canvasOffscreen.height = dh;
canvasOffscreen.getContext('2d').drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
 
// 在绘制每一帧的时候,绘制这个图形
context.drawImage(canvasOffscreen, x, y);

离屏绘制的好处远不止上述。有时候,游戏对象是多次调用 drawImage 绘制而成,或者根本不是图片,而是使用路径绘制出的矢量形状,那么离屏绘制还能帮你把这些操作简化为一次 drawImage 调用。

第一次看到 getImageDataputImageData 这一对 API,我有一种错觉,它们简直就是为了上面这个场景而设计的。前者可以将某个 Canvas 上的某一块区域保存为 ImageData 对象,后者可以将 ImageData 对象重新绘制到 Canvas 上面去。但实际上,putImageData 是一项开销极为巨大的操作,它根本就不适合在每一帧里面去调用。

User Timing

我们还可以使用 Nolan Lawson 推荐的User Timing API来评估语法解析的时间。不过这种方式可能会受 V8 预解析过程的影响,我们可以借鉴 Nolan 在 optimize-js 评测中的方式,在脚本的尾部添加随机字符串来解决这个问题。我基于 Google Analytics 使用相似的方式来评估真实用户与设备访问网站时候的解析时间:
图片 23

响应式图片

十年前,我们使用一种分辨率,就可以为所有人服务,但时代变化太快,现今的响应式 Web 已非往日可比。这也是为什么我们必须要特别留心,去精心优化视觉资源,确保它们适应各种视口设备。幸运的是,感谢 Responsive Images 社区小组,我们可以完美使用 picture 元素和 srcset 属性(二者都有85%+支持率)。

避免「阻塞」

所谓「阻塞」,可以理解为不间断运行时间超过 16ms 的 JavaScript 代码,以及「导致浏览器花费超过 16ms 时间进行处理」的 JavaScript 代码。即使在没有什么动画的页面里,阻塞也会被用户立刻察觉到:阻塞会使页面上的对象失去响应——按钮按不下去,链接点不开,甚至标签页都无法关闭了。而在包含较多 JavaScript 动画的页面里,阻塞会使动画停止一段时间,直到阻塞恢复后才继续执行。如果经常出现「小型」的阻塞(比如上述提及的这些优化没有做好,渲染一帧的时间超过 16ms),那么就会出现「丢帧」的情况,

CSS3 动画(transitionanimate)不会受 JavaScript 阻塞的影响,但不是本文讨论的重点。

图片 24

偶尔的且较小的阻塞是可以接收的,频繁或较大的阻塞是不可以接受的。也就是说,我们需要解决两种阻塞:

  • 频繁(通常较小)的阻塞。其原因主要是过高的渲染性能开销,在每一帧中做的事情太多。
  • 较大(虽然偶尔发生)的阻塞。其原因主要是运行复杂算法、大规模的 DOM 操作等等。

对前者,我们应当仔细地优化代码,有时不得不降低动画的复杂(炫酷)程度,本文前几节中的优化方案,解决的就是这个问题。

而对于后者,主要有以下两种优化的策略。

  • 使用 Web Worker,在另一个线程里进行计算。
  • 将任务拆分为多个较小的任务,插在多帧中进行。

Web Worker 是好东西,性能很好,兼容性也不错。浏览器用另一个线程来运行 Worker 中的 JavaScript 代码,完全不会阻碍主线程的运行。动画(尤其是游戏)中难免会有一些时间复杂度比较高的算法,用 Web Worker 来运行再合适不过了。

图片 25

然而,Web Worker 无法对 DOM 进行操作。所以,有些时候,我们也使用另一种策略来优化性能,那就是将任务拆分成多个较小的任务,依次插入每一帧中去完成。虽然这样做几乎肯定会使执行任务的总时间变长,但至少动画不会卡住了。

图片 26

看下面这个 Demo,我们的动画是使一个红色的 div 向右移动。Demo 中是通过每一帧改变其 transform 属性完成的(Canvas 绘制操作也一样)。

然后,我创建了一个会阻塞浏览器的任务:获取 4×106Math.random() 的平均值。点击按钮,这个任务就会被执行,其结果也会打印在屏幕上。

图片 27

如你所见,如果直接执行这个任务,动画会明显地「卡」一下。而使用 Web Worker 或将任务拆分,则不会卡。

以上两种优化策略,有一个相同的前提,即任务是异步的。也就是说,当你决定开始执行一项任务的时候,你并不需要立刻(在下一帧)知道结果。比如,即使战略游戏中用户的某个操作触发了寻路算法,你完全可以等待几帧(用户完全感知不到)再开始移动游戏角色。
另外,将任务拆分以优化性能,会带来显著的代码复杂度的增加,以及额外的开销。有时候,我觉得也许可以考虑优先砍一砍需求。

DeviceTiming

Etsy 的 DeviceTiming 工具能够模拟某些受限环境来评估页面的语法解析与执行时间。其将本地脚本包裹在了某个仪表工具代码内从而使我们的页面能够模拟从不同的设备中访问。可以阅读 Daniel Espeset 的Benchmarking JS Parsing and Execution on Mobile Devices 一文来了解更详细的使用方式。
图片 28

srcset 属性

srcset在分辨率切换方案中效果最佳——即当我们需要根据用户的屏幕密度和大小显示图像时。基于srcsetsize属性中的一组预定义规则,浏览器将选择最佳图片,相应地提供给视口。这项技术可以带来很大的带宽和请求节省,特别是对于移动用户。
图片 29
[srcset 使用示例]

小结

正文就到这里,最后我们来稍微总结一下,在大部分情况下,需要遵循的「最佳实践」。

  1. 将渲染阶段的开销转嫁到计算阶段之上。
  2. 使用多个分层的 Canvas 绘制复杂场景。
  3. 不要频繁设置绘图上下文的 font 属性。
  4. 不在动画中使用 putImageData 方法。
  5. 通过计算和判断,避免无谓的绘制操作。
  6. 将固定的内容预先绘制在离屏 Canvas 上以提高性能。
  7. 使用 Worker 和拆分任务的方法避免复杂算法阻塞动画运行。

    1 赞 5 收藏 评论

图片 30

我们可以做些什么以降低 JavaScript 的解析时间?

  • 减少 JavaScript 包体体积。我们在上文中也提及,更小的包体往往意味着更少的解析工作量,也就能降低浏览器在解析与编译阶段的时间消耗。
  • 使用代码分割工具来按需传递代码与懒加载剩余模块。这可能是最佳的方式了,类似于PRPL这样的模式鼓励基于路由的分组,目前被 Flipkart, Housing.com 与 Twitter 广泛使用。
  • Script streaming: 过去 V8 鼓励开发者使用async/defer来基于script streaming实现 10-20% 的性能提升。这个技术会允许 HTML 解析器将相应的脚本加载任务分配给专门的 script streaming 线程,从而避免阻塞文档解析。V8 推荐尽早加载较大的模块,毕竟我们只有一个 streamer 线程。
  • 评估我们依赖的解析消耗。我们应该尽可能地选择具有相同功能但是加载地更快的依赖,譬如使用 Preact 或者 Inferno 来代替 React,二者相较于 React 体积更小具有更少的语法解析与编译时间。Paul Lewis 在最近的一篇文章中也讨论了框架启动的代价,与 Sebastian Markbage 的说法不谋而合:最好地评测某个框架启动消耗的方式就是先渲染一个界面,然后删除,最后进行重新渲染。第一次渲染的过程会包含了分析与编译,通过对比就能发现该框架的启动消耗。

如果你的 JavaScript 框架支持 AOT(ahead-of-time)编译模式,那么能够有效地减少解析与编译的时间。Angular 应用就受益于这种模式:
图片 31

picture 元素

picture元素和media属性旨在使艺术设计变得容易。通过为不同情形提供不同图片(通过媒体查询进行测试),无论什么分辨率,我们都能始终将图像中最重要的元素保持在焦点。
图片 32
[picture 元素使用示例]

请务必阅读 Jason Grigsby 的 Responsive Images 101指南,以便对这两种方法进行彻底的阐述。

本文由澳门新葡亰手机版发布于web前端,转载请注明出处:启动性能瓶颈分析与解决方案,最佳实践

上一篇:没有了 下一篇:没有了
猜你喜欢
热门排行
精彩图文