性能

介绍 UI 的渲染流程和渲染性能优化方法。

用户希望他们使用的图形界面具有交互性和流畅性,而这正是你需要越来越多地集中时间和精力的地方。 界面不仅要加载快,还要运行良好, 滚动应该很快,动画和交互应该如丝般流畅。

要编写高性能应用程序,你需要了解 LCUI 如何渲染界面,并确保你编写的代码以及第三方代码尽可能高效地运行。

像素管道

“渲染”就是将组件数据转变为像素数据,这个转变如同一条包含很多区域的单向管道,组件数据经过管道中的每个区域的处理最终变成像素数据。我们可以将这个管道称为像素管道,它的结构如下图所示:

像素管道的各个区域
  • 事件:界面的更新是由事件驱动的,通常我们会在事件处理器中实现一些操作和视觉变化的效果。比如显示一个加载中动画、切换到另一个界面、或者往界面里添加一些内容等。

  • 样式计算:此过程是根据匹配选择器(例如 .button.list .list-item)计算出哪些组件应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。

  • 布局: 在知道对一个组件应用哪些规则之后,LCUI 即可开始计算它要占据的空间大小及其在屏幕的位置。LCUI 所采用的类似于网页的布局模式意味着一个组件可能影响其他组件,例如更改组件的宽度会影响到子组件的位置和宽度以及组件树中各处的节点,因此布局过程是经常发生的。

  • 绘制:绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。

  • 合成:由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染界面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

管道的每个部分都有机会产生卡顿,因此务必准确了解你的代码触发管道的哪些部分。

不一定每帧都总是会经过管道每个部分的处理。在实现视觉变化时,管道针对指定帧的运行通常有三种方式:

1. 事件 > 样式 > 布局 > 绘制 > 合成

如果你修改了组件的布局属性,即改变了组件的几何属性(例如宽度、高度、左侧或顶部位置等),那么 LCUI 将必须检查所有其它组件,然后对界面进行重新布局。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

2. 事件 > 样式 > 绘制 > 合成

如果你修改了组件的绘制属性,即不会影响界面布局的属性(例如背景图片、文字颜色或阴影等),则 LCUI 会跳过布局,但仍将执行绘制。

3. 事件 > 样式

如果您更改组件的一个既不要布局也不要绘制的属性(例如:pointer-events),则 LCUI 将在计算完样式后跳过剩下的过程。

接下来,让我们深入了解此管道的各个不同部分。我们会以一些常见问题为例,阐述如何诊断和修正它们。

事件

事件处理器经常会触发视觉变化。有时是直接通过样式操作,有时是会产生视觉变化的计算,例如搜索数据或将其排序。时机不当或长时间运行的代码可能是导致性能问题的常见原因。您应当设法尽可能减少其影响。

在许多情况下,你可以将事件处理器中的耗时长的代码从主线程移动到工作线程,详见工作线程章节。不过在工作线程上你必须确保这些代码不会操作 UI 相关资源, 如果你的工作必须在主线程上执行,请考虑一种分批处理的方法,将大任务分割为小任务,每个小任务所占时间不超过几毫秒,然后使用定时器逐个执行这些任务。

样式计算

通过添加和删除组件,更改属性、类来更改组件,都会导致 LCUI 重新计算组件样式,在很多情况下还会对整个界面或界面的一部分进行布局。

计算样式的第一部分是创建一组匹配选择器,也就是计算出给指定元素应用哪些类、伪选择器和 ID。第二部分涉及从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。

概要:

  • 降低选择器的复杂度;使用 BEM 这种以类为中心的方法论。

  • 为数量多且样式相同的组件预先生成样式哈希值,以减少重复的样式匹配。

  • 为含有大量子组件的容器组件设置更新规则,告诉 LCUI 是否需要缓存样式表、是否仅更新可见的子组件、哪些变动可以忽略等。

  • 减少需要计算其样式的组件数量。

布局

布局是计算各组件几何信息的过程:组件的大小以及在界面中的位置。根据所用的 CSS、组件的内容或父级组件,每个组件都将有显式或隐含的大小信息。

与样式计算相似,布局开销的直接考虑因素如下:

  1. 需要布局的元素数量。

  2. 这些布局的复杂度。

概要:

  • 组件的数量将影响性能;应尽可能避免触发重新布局。

  • 评估布局模型的性能;弹性盒子(Flexbox)一般比块(Block)布局模型更慢。

  • 宽高为固定值的组件有着较低的重新布局成本;这种组件在重新布局时无需再遍历子组件树来计算内容宽高,而且能减少因其父组件和子组件的几何属性变化而触发的重新布局次数。

绘制

绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是管道中运行时间最长的任务,应尽可能避免此任务。

概要:

  • 更改任何属性始都会触发绘制。

  • 绘制通常是像素管道中开销最大的部分;应尽可能避免绘制。

  • 大量且尺寸小的绘制区域会降低绘制性能,但好在 LCUI 已经针对这种情况做了区域合并和多个区域并行绘制等优化,大部分情况下你不用考虑这个问题。

  • 设置 opacity 和 box-shadow 属性会触发使用独立渲染层,组件及其子组件都被在该渲染层中绘制,在全部绘制完后,该渲染层的内容才会被合成到目标面上。

合成

由于 LCUI 还未引入动画和变换系统,我们先暂时跳过这方面的讲解。

参考资料

本文的内容结构和表达方式参考自《渲染性能 | Web | Google Developer》。

待办事项

添加 LCUI_RequestAnimationFrame() 函数

参考 window.requestAnimationFrame 的设计。

添加渲染性能监视器

详见 #192 中的内容。