精华内容
下载资源
问答
  • 2019-01-14 09:51:43

    摘要: 理解浏览器渲染。

    Fundebug经授权转载,版权归原作者所有。

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第11篇。

    如果你错过了前面的章节,可以在这里找到它们:

    当你构建 Web 应用程序时,你不只是编写单独运行的 JavaScript 代码,你编写的 JavaScript 正在与环境进行交互。了解这种环境,它的工作原理以及它的组,这些有助于你够构建更好的应用程序,并为应用程序发布后可能出现的潜在问题做好充分准备。

    浏览器的主要组件包括:

    • 用户界面 (User interface): 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分
    • **浏览器引擎 (Browser engine):**用来查询及操作渲染引擎的接口
    • **渲染引擎 (Rendering engine):**用来显示请求的内容,例如,如果请求内容为 html,它负责解析 html 及 css,并将解析后的结果显示出来
    • **网络 (Networking):**用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作
    • **UI 后端 (UI backend):**用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
    • **JS 解释器 (JavaScript engine):**用来解释执行JS代码
    • 数据存储 (Data persistence): 属于持久层,浏览器需要在硬盘中保存类似 cookie 的各种数据,HTML5定义了 Web Database 技术,这是一种轻量级完整的客户端存储技术,支持的存储机制类型包括 localStorageindexDBWebSQLFileSystem

    在这篇文章中,将重点讨论渲染引擎,因为它处理 HTML 和 CSS 的解析和可视化,这是大多数 JavaScript 应用程序经常与之交互的东西。

    渲染引擎概述

    渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。

    渲染引擎可以显示 HTML 和 XML 文档和图像。如果使用其他插件,渲染引擎还可以显示不同类型的文档,如 PDF。

    渲染引擎 (Rendering engines)

    与 JavaScript 引擎类似,不同的浏览器也使用不同的渲染引擎。以下是一些最受欢迎的:

    • Gecko — Firefox
    • WebKit — Safari
    • Blink — Chrome,Opera (版本 15 之后)

    Firefox、Chrome 和 Safari 是基于两种渲染引擎构建的,Firefox 使用 Geoko——Mozilla 自主研发的渲染引擎,Safari 和 Chrome 都使用 Webkit。Blink 是 Chrome 基于 WebKit的自主渲染引擎。

    渲染的过程

    渲染引擎从网络层接收所请求文档的内容。

    解析 HTML 以构建 Dom 树 -> 构建 Render 树 -> 布局 Render 树 -> 绘制 Render 树

    构建 Dom 树

    渲染现引擎的第一步是解析 HTML文档,并将解析后的元素转换为 DOM 树中的实际 DOM 节点。

    假如有如下 Html 结构

    <html>
      <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" type="text/css" href="theme.css">
      </head>
      <body>
        <p> Hello, <span> friend! </span> </p>
        <div> 
          <img src="smiley.gif" alt="Smiley face" height="42" width="42">
        </div>
      </body>
    </html>
    

    对应的 DOM 树如下:

    基本上,每个元素都表示为所有元素的父节点,这些元素直接包含在元素中。

    构建 CSSOM

    CSSOM 指的是 CSS 对象模型。 当浏览器构建页面的 DOM 时,它在 head 标签下如遇到了一个 link 标记且引用了外部 theme.css CSS 样式表。 浏览器预计可能需要该资源来呈现页面,它会立即发送请求。 假设 theme.css 文件内容如下:

    body { 
      font-size: 16px;
    }
    
    p { 
      font-weight: bold; 
    }
    
    span { 
      color: red; 
    }
    
    p span { 
      display: none; 
    }
    
    img { 
      float: right; 
    }
    

    与 HTML一样,渲染引擎需要将 CSS 转换成浏览器可以使用的东西—— CSSOM。CSSOM 结构如下:

    你想知道为什么 CSSOM 是一个树形结构? 在为页面上的任何对象计算最终样式集时,浏览器以适用于该节点的最常规规则开始(例如,如果它是 body 元素的子元素,则应用所有 body 样式),然后递归地细化,通过应用更具体的规则来计算样式。

    来看看具体的例子。包含在 body 元素内的 span 标签中的任何文本的字体大小均为 16 像素,并且为红色。这些样式是从 body 元素继承而来的。 如果一个 span 元素是一个 p 元素的子元素,那么它的内容就不会被显示,因为它被应用了更具体的样式(display: none)。

    另请注意,上面的树不是完整的 CSSOM 树,只显示我们决定在样式表中覆盖的样式。 每个浏览器都提供一组默认样式,也称为**“user agent stylesheet”**。这是我们在未明确指定任何样式时看到的样式,我们的样式会覆盖这些默认值。

    不同浏览器对于相同元素的默认样式并不一致,这也是为什么我们在 CSS 的最开始要写 *{padding:0;marging:0};,也就是我们要重置CSS默认样式的。

    构建渲染树

    CSSOM 树和 DOM 树连接在一起形成一个 render tree,渲染树用来计算可见元素的布局并且作为将像素渲染到屏幕上的过程的输入。

    • DOM 树和 CSSOM 树连接在一起形成 render tree .
    • render tree 只包含了用于渲染页面的节点
    • 布局计算了每一个对象的准确的位置以及大小
    • 绘画是最后一步,绘画要求利用 render tree 来将像素显示到屏幕上

    渲染树中的每个节点在 Webkit 中称为渲染器或渲染对象。

    收下是上面 DOM 和 CSSOM 树的渲染器树的样子:

    为了构建渲染树,浏览器大致执行以下操作:

    • 从 DOM 树根节点开始,遍历每一个可见的节点
      • 一些节点是完全不可见的(比如 script标签,meta标签等),这些节点会被忽略,因为他们不会影响渲染的输出
      • 一些节点是通过 CSS 样式隐藏了,这些节点同样被忽略——例如上例中的 span 节点在 render tree 中被忽略,因为 span 样式是 display:none
    • 对每一个可见的节点,找到合适的匹配的CSSOM规则,并且应用样式
    • 显示可见节点(节点包括内容和被计算的样式)

    “visibility:hidden”“display:none” 之间的不同,“visibility:hidden” 将元素设置为不可见,但是同样在布局上占领一定空间(例如,它会被渲染成为空盒子),但是 “display:none” 的元素是将节点从整个 render tree 中移除,所以不是布局中的一部分 。

    你可以在这里查看 RenderObject 的源代码(在 WebKit 中):

    https://github.com/WebKit/web…

    我们来看看这个类的一些核心内容:

    每个渲染器代表一个矩形区域,通常对应于一个节点的 CSS 盒模型。它包含几何信息,例如宽度、高度和位置。

    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

    渲染树的布局

    创建渲染器并将其添加到树中时,它没有位置和大小,计算这些值称为布局。

    HTML使用基于流的布局模型,这意味着大多数时间它可以一次性计算几何图形。坐标系统相对于根渲染器,使用左上原点坐标。

    布局是一个递归过程 - 它从根渲染器开始,它对应于 HTML 文档的 <html> 元素。 布局以递归方式继续通过部件或整个渲染器层次结构,为每个需要它的渲染器计算几何信息。

    根渲染器的位置为0,0,其尺寸与浏览器窗口的可见部分(即viewport)的大小相同。开始布局过程意味着给每个节点在屏幕上应该出现的确切坐标。

    绘制渲染树

    在此绘制,遍历渲染器树并调用渲染器的 paint() 方法以在屏幕上显示内容。

    绘图可以是全局的或增量式的(与布局类似):

    • 全局 — 整棵树被重绘
    • 增量式 — 只有一些渲染器以不影响整个树的方式改变。 渲染器使其在屏幕上的矩形无效,这会导致操作系统将其视为需要重新绘制并生成绘 paint 事件的区域。 操作系统通过将多个区域合并为一个来智能完成。

    总的来说,重要的中要理解绘图是一个渐进的过程。为了更好的用户体验,渲染引擎将尽可能快地在屏幕上显示内容。它不会等到解析完所有 HTML 后才开始构建和布局渲染树,而是解析和显示部分内容,同时继续处理来自网络的其余内容项。

    处理脚本和样式表的顺序

    当解析器到达 <script> 标记时,将立即解析并执行脚本。文档的解析将暂停,直到执行脚本为止。这意味着这个过程是同步的

    如果脚本是外部的,那么首先必须从网络中获取它(也是同步的)。所有解析都停止,直到获取完成。HTML5 新加了async 或 defer 属性,将脚本标记为异步的,以便由不同的线程解析和执行。

    优化渲染性能

    如果你想优化自己的应用,则需要关注五个主要方面,这些是你自己可以控制的:

    1. JavaScript   — 在之前的文章中,讨论了如果编写优化代码的主题抱包括如果编写代码才不会阻止UI,和提高内存利用等等。在渲染时,需要考虑 JavaScript 代码与页面 上DOM 素交互的方式。 JavaScript 可以在 UI中创建大量更改,尤其是在 SPA 中。
    2. 样式计算 — 这是根据匹配选择器确定哪个 CSS 规则适用于哪个元素的过程。 定义规则后,将应用它们并计算每个元素的最终样式。
    3. 布局 — 一旦浏览器知道哪些规则适用于某个元素,它就可以开始计算后者占用多少空间以及它在浏览器屏幕上的位置。Web 的布局模型定义了一个元素可以影响其他元素。例如, 的宽度会影响其子元素的宽度,等等。这意味着布局过程是计算密集型的,该绘图是在多个图层完成的。
    4. 绘图 —— 这是实际像素被填充的地方,这个过程包括绘制文本、颜色、图像、边框、阴影等——每个元素的每个可视部分。
    5. 合成  — 由于页面部分可能被绘制成多个层,因此它们需要以正确的顺序绘制到屏幕上,以便页面渲染正确。这是非常重要的,特别是对于重叠的元素。

    优化你的 JavaScript

    JavaScript 经常触发浏览器中的视觉变化,构建 SPA 时更是如此。

    以下是一些优化 JavaScript 渲染技巧:

    • 避免使用 setTimeoutsetInterval 进行可视更新。 这些将在帧中的某个点调用 callback ,可能在最后。我们想要做的是在帧开始时触发视觉变化而不是错过它。
    • 之前文章 所述,将长时间运行的 JavaScript 计算转移到 Web Workers。
    • 使用微任务在多个帧中变更 DOM。这是在任务需要访问 DOM 时使用的, Web Worker 无法访问 DOM。这基本上意味着你要把一个大任务分解成更小的任务,然后根据任务的性质在 requestAnimationFrame, setTimeout, setInterval 中运行它们。

    优化你的 CSS

    通过添加和删除元素,更改属性等来修改 DOM 将使浏览器重新计算元素样式,并且在许多情况下,重新计算整个页面的布局或至少部分布局。

    要优化渲染,考虑以下事项:

    • 减少选择器的复杂性,与构造样式本身的其他工作相比,选择器复杂性可以占用计算元素样式所需时间的50%以上。

    * 减少必须进行样式计算的元素的数量。本质上,直接对一些元素进行样式更改,而不是使整个页面无效。

    优化布局

    浏览器的布局重新计算可能非常繁重。 考虑以下优化:

    • 尽可能减少布局的数量。当你更改样式时,浏览器会检查是否有任何更改需要重新计算布局。对宽度、高度、左、顶等属性的更改,以及通常与几何相关的属性的更改,都需要布局。所以,尽量避免改变它们。
    • 尽量使用 flexbox 而不是老的布局模型。它运行速度更快,可为你的应用程序创造巨大的性能优势。
    • 避免强制同步布局。需要记住的是,在 JavaScript 运行时,前一帧中的所有旧布局值都是已知的,可以查询。如果你访问 box.offsetHeight,那就不成问题了。但是,如果你在访问 box 之前更改了它的样式(例如,通过动态地向元素添加一些 CSS 类),浏览器必须先应用样式更改并执行布局过程,这是非常耗时和耗费资源的,所以尽可能避免。

    优化绘图

    这通常是所有任务中运行时间最长的,因此尽可能避免这种情况非常重要。 以下是我们可以做的事情:

    • 除了变换(transform)和透明度之外,改变其他任何属性都会触发重新绘图,请谨慎使用。
    • 如果触发了布局,那也会触发绘图,因为更改布局会导致元素的视觉效果也改变。
    • 通过图层提升和动画编排来减少重绘区域。

    原文:

    https://blog.sessionstack.com…

    关于Fundebug

    Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用

    更多相关内容
  • 一个 Android 应用是否流畅,或者说是否存在卡顿、丢帧...为什么是 60fps? 12fps(帧/秒) 由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约 10 - 12fps 的时候,就会认为是连贯的。 早期的无声电影...

    文章首发「Android波斯湾」公众号,更新地址:https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode

    一个 Android 应用是否流畅,或者说是否存在卡顿、丢帧现象,都与 60fps 和 16ms 有关。那么这两个值是怎么来的呢?为什么以这两个值为衡量标准呢?本文主要讨论下渲染性能方面决定 Android 应用流畅性的因素。

    为什么是 60fps?

    • 12fps(帧/秒)

    由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约 10 - 12fps 的时候,就会认为是连贯的。 早期的无声电影的帧率介于 16 - 24fps 之间,虽然帧率足以让人感觉到运动,但往往被认为是在快放幻灯片。 在 1920 年代中后期,无声电影的帧率提高到 20 - 26fps 之间。

    • 24fps

    1926 年有声电影推出,人耳对音频的变化更敏感,反而削弱了人对电影帧率的关注。因为许多无声电影使用 20 - 26fps 播放,所以选择了中间值 24fps 作为有声电影的帧率。 之后 24fps 成为 35mm 有声电影的标准。

    • 30fps

    早期的高动态电子游戏,帧率少于每秒 30fps 的话就会显得不连贯。这是因为没有动态模糊使流畅度降低。 (注:如果需要了解动态模糊技术相关知识,可以查阅 这里

    • 60fps

    在实际体验中,60fps 相对于 30fps 有着更好的体验。

    • 85fps

    一般而言,大脑处理视频的极限。

    所以,总体而言,帧率越高体验越好。 一般的电影拍摄及播放帧率均为每秒 24 帧,但是据称《霍比特人:意外旅程》是第一部以每秒 48 帧拍摄及播放的电影,观众认为其逼真度得到了显著的提示。

    目前,大多数显示器根据其设定按 30Hz、 60Hz、 120Hz 或者 144Hz 的频率进行刷新。 而其中最常见的刷新频率是 60Hz。

    这样做是为了继承以前电视机刷新频率为 60Hz 的设定。 而 60Hz 是美国交流电的频率,电视机如果匹配交流电的刷新频率就可以有效的预防屏幕中出现滚动条,即互调失真。

    16 ms

    正如上面所述目前大多数显示器的刷新率是 60Hz,Android 设备的刷新率也是 60Hz。只有当画面达到 60fps 时 App 应用才不会让用户感觉到卡顿。那么 60fps 也就意味着 1000ms/60Hz = 16ms。也就是说 16ms 渲染一次画面才不会卡顿。

    CPU vs GPU

    渲染操作通常依赖于两个核心组件:CPU 与 GPU。CPU 负责包括 Measure、Layout、Record、Execute 的计算操作,GPU 负责 Rasterization (栅格化)操作。

    CPU 通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的 GPU 资源。

    CPU vs GPU

    Android UI 与 GPU

    了解 Android 是如何利用 GPU 进行画面渲染有助于我们更好的理解性能问题。

    那么一个最实际的问题是:Activity 的画面是如何绘制到屏幕上的?那些复杂的 XML 布局文件又是如何能够被识别并绘制出来的?

    Resterization

    Resterization 栅格化是绘制那些 Button、Shape、Path、String、Bitmap 等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。

    这是一个很费时的操作,GPU 的引入就是为了加快栅格化的操作。

    CPU 负责把 UI 组件计算成 Polygons,Texture 纹理,然后交给 GPU 进行栅格化渲染。

    CPU 与 GPU 工作流程

    然而,每次从 CPU 转移到 GPU 是一件很麻烦的事情,所幸的是 OpenGL ES 可以把那些需要渲染的纹理缓存在 GPU Memory 里面,在下次需要渲染的时候可以直接使用。但是,如果你更新了 GPU 缓存的纹理内容,那么之前保存的状态就丢失了。

    在 Android 里面那些由主题所提供的资源(例如:Bitmaps、Drawables)都是一起打包到统一的 Texture 纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。

    当然,随着 UI 组件的越来越丰富,有了更多演变的形态。例如,显示图片的时候,需要先经过 CPU 的计算加载到内存中,然后传递给 GPU 进行渲染。文字的显示更加复杂,需要先经过 CPU 换算成纹理,然后再交给 GPU 进行渲染,回到 CPU 绘制单个字符的时候,再重新引用经过 GPU 渲染的内容。动画则是一个更加复杂的操作流程。

    为了能够使得 App 流畅,我们需要在每一帧 16ms 以内处理完所有的 CPU 与 GPU 计算,绘制,渲染等等操作。

    UI 组件的更新

    通常来说,Android 需要把 XML 布局文件转换成 GPU 能够识别并绘制的对象。这个操作是在 DisplayList 的帮助下完成的。DisplayList 持有所有将要交给 GPU 绘制到屏幕上的数据信息。

    在某个 View 第一次需要被渲染时,DisplayList 会因此而被创建。当这个 View 要显示到屏幕上时,我们会执行 GPU 的绘制指令来进行渲染。

    如果你在后续有执行类似移动这个 View 的位置等操作而需要再次渲染这个 View 时,我们就仅仅需要额外操作一次渲染指令就够了。然而如果你修改了 View 中的某些可见组件,那么之前的 DisplayList 就无法继续使用了,我们需要回头重新创建一个 DisplayList 并且重新执行渲染指令并更新到屏幕上。

    需要注意的是:任何时候 View 中的绘制内容发生变化时,都会重新执行创建 DisplayList,渲染 DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于你的 View 的复杂程度,View 的状态变化以及渲染管道的执行性能。

    UI 组件的更新

    举个例子,假设某个 Button 的大小需要增大到目前的两倍,在增大 Button 大小之前,需要通过父 View 重新计算并摆放其他子 View 的位置。修改 View 的大小会触发整个 HierarcyView 的重新计算大小的操作。如果是修改 View 的位置则会触发 HierarchView 重新计算其他 View 的位置。如果布局很复杂,这就会很容易导致严重的性能问题。

    垂直同步

    为了理解 App 是如何进行渲染的,我们必须了解手机硬件是如何工作,那么就必须理解什么是垂直同步(VSYNC)。

    在讲解 VSYNC 之前,我们需要了解两个相关的概念:

    刷新率

    刷新率(Refresh Rate)代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如 60Hz。

    帧率

    帧率(Frame Rate)代表了 GPU 在一秒内绘制操作的帧数,例如 30fps,60fps。

    GPU 会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。

    GPU 渲染

    玩游戏的同学,尤其是大型 FPS 游戏应该都见过「垂直同步」这个选项。因为 GPU 的生成图像的频率与显示器的刷新频率是相互独立的,所以就涉及到了一个配合的问题。

    最理想的情况是两者之间的频率是相同且协同进行工作的,在这样的理想条件下,达到了最优解。

    GPU 帧率

    但实际中 GPU 的生成图像的频率是变化的,如果没有有效的技术手段进行保证,两者之间很容易出现这样的情况。

    当 GPU 还在渲染下一帧图像时,显示器却已经开始进行绘制,这样就会导致屏幕撕裂(Tear)。这会使得屏幕的一部分显示的是前一帧的内容,而另一部分却在显示下一帧的内容。如下图所示:

    撕裂的图像

    屏幕撕裂(Tear)的问题,早在 PC 游戏时代就被发现, 并不停的在尝试进行解决。 其中最知名可能也是最古老的解决方案就是 VSYNC 技术。

    VSYNC 的原理简单而直观:产生屏幕撕裂的原因是 GPU 在屏幕刷新时进行了渲染,而 VSYNC 通过同步渲染/刷新时间的方式来解决这个问题。

    显示器的刷新频率为 60Hz,若此时开启 VSYNC,将控制 GPU 渲染速度在 60Hz 以内以匹配显示器刷新频率。这也意味着,在 VSYNC 的限制下,GPU 显示性能的极限就限制为 60Hz 以内。这样就能很好的避免图像撕裂的问题。

    通常来说,帧率超过刷新频率只是一种理想的状况,在超过 60fps 的情况下,GPU 所产生的帧数据会因为等待 VSYNC 的刷新信息而被 Hold 住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。

    VSYNC

    在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过 60fps 突然掉到 60fps 以下,这样就会发生 LAG、JANK、HITCHING 等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。

    渲染性能

    大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能(Rendering Performance)。

    从设计师的角度,他们希望 App 能够有更多的动画,图片等时尚元素来实现流畅的用户体验。但是 Android 系统很有可能无法及时完成那些复杂的界面渲染操作。

    Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的 60fps,为了能够实现 60fps,这意味着程序的大多数操作都必须在 16ms 内完成。

    VSYNC 信号

    如果你的某个操作花费时间是 24ms,系统在得到 VSYNC 信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在 32ms 内看到的会是同一帧画面。

    丢帧

    用户容易在 UI 执行动画或者滑动 ListView 的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。

    有很多原因可以导致丢帧,也许是因为你的 layout 太过复杂,无法在 16ms 内完成渲染,有可能是因为你的 UI 上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致 CPU 或者 GPU 负载过重。

    过度重绘

    过度重绘(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的 UI 也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的 CPU 以及 GPU 资源。

    Overdraw

    当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少 Overdraw 的情况发生。

    如何找出过度重绘?

    很荣幸 Android 系统的开发者模式中,提供了一些工具可以帮助我们找出过度重绘。

    首先,打开手机里面的开发者选项(这个都找不到,那还开发什么 Android?),可以找到下面几个选项:

    调试 GPU 过度重绘(Debug GPU overdraw)

    我们可以通过手机设置里面的 开发者选项 ,打开 显示过渡绘制区域(Show GPU Overdraw)的选项,可以观察 UI 上的 Overdraw 情况。

    Debug GPU overdraw

    蓝色,淡绿,淡红,深红代表了 4 种不同程度的 Overdraw 情况,我们的目标就是尽量减少红色 Overdraw,看到更多的蓝色区域。

    • 真彩色:没有过度绘制
    • 蓝色:过度重绘 1 次

    像素绘制了 2 次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。

    • 绿色:过度重绘 2 次

    像素绘制了 3 次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。

    • 淡红: 过度重绘 3 次

    像素绘制了 4 次,小范围可以接受。

    • 深红: 过度重绘 4 次或更多

    像素绘制了 5 次或者更多。这是错误的,要修复它们。

    Overdraw 有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。

    例如:某个 Activity 有一个背景,然后里面的 Layout 又有自己的背景,同时子 View 又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色 Overdraw 区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。

    优化过度重绘

    GPU 呈现模式分析(Profile GPU Rendering)

    我们可以通过手机设置里面的 开发者选项 中找到 GPU 呈现模式分析(Peofile GPU Rendering tool) ,然后选择 在屏幕上显示为条形图(On screen as bars)。

    Profile GPU Rendering

    在 Android 系统中是以 60fps 为满帧,绿色横线为 16ms 分界线,低于绿线即为流畅。

    屏幕下方的柱状图每一根代表一帧,其高度表示「渲染这一帧耗时」,随着手机屏幕界面的变化,柱状图会持续刷新每帧用时的具体情况(通过高度表示)。

    那么,当柱状图高于绿线,是不是就说明我卡了呢?其实这不完全正确,这里就要开始分析组成每一根柱状图不同颜色所代表的含义了。

    gpu 16ms

    • 红色

    代表了「执行时间」,它指的是 Android 渲染引擎执行盒子中这些绘制命令的时间。

    假如当前界面的视图越多,那么红色便会「跳」得越高。实际使用中,比如我们平时刷淘宝 App 时遇到出现多张缩略图需要加载时,那么红色会突然跳很高,但是此时你的页面滑动其实是流畅的,虽然等了零点几秒图片才加载出来,但其实这可能并不意味着你卡住了。

    • 黄色

    通常较短,它代表着 CPU 通知 GPU 你已经完成视图渲染了,不过在这里 CPU 会等待 GPU 的回话,当 GPU 说「好的知道了」,才算完事儿。

    假如橙色部分很高的话,说明当前 GPU 过于忙碌,有很多命令需要去处理,比如 Android 淘宝客户端,红色黄色通常会很高。

    • 蓝色

    假如想通过玄学曲线来判断流畅度的话,其实蓝色的参考意义是较大的。蓝色代表了视图绘制所花费的时间,表示视图在界面发生变化(更新)的用时情况。

    当它越短时,即便是体验上更接近「丝滑」,当他越长时,说明当前视图较复杂或者无效需要重绘,即我们通常说的「卡了」。

    理解了玄学曲线不同颜色代表的意义,看懂玄学曲线就不难了。 一般情况下,当蓝色低于绿线时都不会出现卡顿,但是想要追求真正的丝般顺滑那当然还是三色全部处于绿线以下最为理想。

    Hierarchy Viewer

    Hierarchy Viewer 是 Android Device Monitor 中的一个工具,它可以帮助我们检测布局层次结构中每个视图的布局速度。

    它的界面如下:

    Hierarchy Viewer

    有一定开发经验的小伙伴应该使用过它,不过现在已经被「弃用了」,Google 推荐我们使用 Layout Inspector 来检查应用程序的视图层次结构。

    Layout Inspector

    Layout Inspector 集成在 Android Studio 中,点击 Tools > Layout Inspector,在出现的 Choose Process 对话框中,选择您想要检查的应用进程,然后点击 OK

    Layout Inspector

    默认情况下,Choose Process 对话框仅会为 Android Studio 中当前打开的项目列出进程,并且该项目必须在设备上运行。

    如果您想要检查设备上的其他应用,请点击 Show all processes。如果您正在使用已取得 root 权限的设备或者没有安装 Google Play 商店的模拟器,那么您会看到所有正在运行的应用。否则,您只能看到可以调试的运行中应用。

    布局检查器会捕获快照,将它保存为 .li 文件并打开。 如图下图所示,布局检查器将显示以下内容:

    Layout Inspector

    优化布局

    使用上面的工具找到了过度重绘的地方,就需要优化自己的代码,我们可以通过下面几个方式进行优化。

    include

    include 标签常用于将布局中的公共部分提取出来供其他 layout 共用,以实现布局模块化。

    merge

    merge 标签主要用于辅助 include 标签,在使用 include 后可能导致布局嵌套过多,多余的 layout 节点或导致解析变慢。

    例如:根布局是 Linearlayout,那么我们又 include 一个 LinerLayout 布局就没意义了,反而会减慢 UI 加载速度。

    ViewStub

    ViewStub 标签最大的优点是当你需要时才会加载,使用它并不会影响UI初始化时的性能。

    例如:不常用的布局像进度条、显示错误消息等可以使用 ViewStub 标签,以减少内存使用量,加快渲染速度.。

    ViewStub 是一个不可见的,实际上是把宽高设置为 0 的 View。效果有点类似普通的 view.setVisible(),但性能体验提高不少。

    ConstraintLayout

    约束布局 ConstraintLayout 是一个 ViewGroup,可以在 API 9 以上的 Android 系统使用它,它的出现主要是为了解决布局嵌套过多的问题,以灵活的方式定位和调整小部件。从 Android Studio 2.3 起,官方的模板默认使用 ConstraintLayout。

    更多使用细节详见:Android 开发文档 - ConstraintLayoutConstraintLayout,看完一篇真的就够了么?

    优化自定义 View

    onDraw()

    减少 onDraw() 耗时操作。

    clipRect() 与 quickReject()

    我们可以通过 canvas.clipRect() 来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。

    这个API可以很好的帮助那些有多组重叠组件的自定义 View 来控制显示的区域。同时 clipRect 方法还可以帮助节约 CPU 与 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

    clipRect

    除了 clipRect 方法之外,我们还可以使用 canvas.quickReject() 来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

    quickReject

    上面的示例图中显示了一个自定义的View,主要效果是呈现多张重叠的卡片。这个 View 的 onDraw 方法如下图所示:

    protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);
      
      if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) {
        // 过度重绘代码
        int i;
        for (i = 0; i < mDroidCards.size(); i++) {
          // 每张卡片都放在前一张卡片的右侧
          mCardLeft = i * mCardSpacing;
          drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);
        }
      }
    
      invalidate();
    }
    

    打开「开发者选项」中的「显示过度渲染」,可以看到我们这个自定义的 View 部分区域存在着过度绘制。

    下面的代码显示了如何通过 clipRect 来解决自定义 View 的过度绘制,提高自定义 View 的绘制性能:

    protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);
    
      if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) {
        int i;
        for (i = 0; i < mDroidCards.size() - 1; i++) {
          // 每张卡片都放在前一张卡片的右侧
          mCardLeft = i * mCardSpacing;
          // 保存 canvas 的状态
          canvas.save();
          // 将绘图区域限制为可见的区域
          canvas.clipRect(mCardLeft, 0, mCardLeft + mCardSpacing, 
                          mDroidCards.get(i).getHeight());
    
          drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);
          // 将画布恢复到非剪切状态
          canvas.restore();
        }
    
        // 绘制最后没有剪裁的卡片
        drawDroidCard(canvas, mDroidCards.get(i), 
                      mCardLeft + mCardSpacing, 0);
      }
    
      invalidate();
    }
    

    避免使用不支持硬件加速的 API

    Android 系统中图形绘制分为两种方式,纯软件绘制和使用硬件加速绘制。

    大家可以查看 美团技术团队 - Android 硬件加速原理与实现简介 这篇文章了解下硬件加速的实现原理。

    简单来说在 Android 3.0(API 11)之前没有硬件加速,图形绘制是纯软件的方式,DisplayList 的生成和绘制都需要 CPU 来完成。之后加入的硬件加速(默认开启)将一部分图形相关的操作交给 GPU 来处理,这样大大减少了 CPU 的运算压力。

    所以我们在开发过程中应尽量避免使用不支持硬件加速的 API,来提升 UI 的渲染性能。

    我的 GitHub

    github.com/jeanboydev

    技术交流群

    欢迎加入技术交流群,来一起交流学习。

    QQ 技术交流群
    微信技术交流群

    我的公众号

    欢迎关注我的公众号,分享各种技术干货,各种学习资料,职业发展和行业动态。

    Android 波斯湾

    参考资料

    展开全文
  • UE4 实时渲染原理&优化策略笔记

    千次阅读 2021-02-02 00:22:37
    (PS:前方高能预警,官方大佬实时渲染进阶教程的笔记总结,大佬对渲染的原理解释很到位,只有知道原理才能理解为啥这样设置就可以优化性能,才能在实战中举一反三,保证干货满满,建议看完视频后再来,或者结合视频...

    目录

    Rendering Before

    几何体渲染

    光栅化和GBuffer

    渲染和纹理

    像素着色器和材质

    反射

    静态光照和静态阴影

    动态光照和动态阴影

    雾和半透明

    后期处理

    [实时渲染补充内容]

    LOD

    贴花Decals

    次表面散射补充


    (PS:官方大佬Sjoerd de Jong <深入实时渲染>课程的笔记总结,大佬对渲染的原理解释很到位,只有了解原理才能理解为啥这样设置就可以优化性能,才能在实战中举一反三

    保证干货满满,建议看完视频后再来,或者结合视频食用...

    附上B站教程链接:https://www.bilibili.com/video/BV1KE411r76i?from=search&seid=4460059173728126740

    Rendering Before

    • GPU要渲染什么,是由CPU进行通知、决定的。首先,CPU必须要知道场景中一切对象的位置,这是场景中一切可呈现物体的基本属性。同时,CPU要负责计算相关逻辑和变换(空间方面),包括但不限于动画、模型和对象的位置、物理效果、AI、对象的生成与销毁、对象的出现与消失、隐藏与取消隐藏等。

    • 渲染遮挡(剔除)优化要点

       

     

    • 设置距离剔除(下图为Actor设置,植被工具中也有CullDistance选项控制)

       

    • 设置预计算可视性体积,需要编译光照

       

    • 命令stat initViews查看预计算cost

    • 命令stat engine查看渲染三角形数目

     

    几何体渲染

    • 相同属性的多面体会调用一次drawcall,同一几何体的不同材质会多次调用

    • basePass负责几何体渲染,gpu逐次调用drawcall渲染并缓存到Gbuffer用于后续处理

    • 查看命令:stat RHI,drawcall调用次数(2000-3000合理,>5000太高)

    • 光照会影响drawcall的调用次数

    • drawcall次数对性能的影响甚至高于多面体模型三角形的个数(高模影响),每次调用都会获取下次渲染信息,调用间隔和传输损耗对影响很大,这意味着过多复杂的材质可能比高精度模型的渲染开销更大

    • RenderDoc插件用于截帧,观察渲染流程:secene列表下每一次都是一次drawcall调用,时钟标记可以看到耗时,依次找出场景中drawcall最耗时的Actor。右边可以预览Gbuffer缓存的内容

    • 静态网格体和下属的组件都会逐个调用drawcall, 减少drawcall次数的两个方法:

      1、合并模型

      2、使用LOD优化

      • 合并或使用少量更大的模型而非大量小型模型

         

        • 大模型减少drawcall调用,但是会带来其他问题:如碰撞检测(CPU计算),遮挡,光照贴图会更复杂

        • 折中策略:使用模块化工作流,即小模型导入,场景搭建中对小模型适度合并,一般远处的模型群需要合并,具体合并策略:

           

          • 同一区域广泛使用(Count)的面数少(Tris)且材质类型相同且合并后面数适中(SumTris)的模型最佳,面数可以使用统计功能筛选:

            window - statistic - primitive stats

             

        • 合并模型来优化drawcall的收益取决于项目的实际帧率需求,权衡时间和收益,否则没有必要过多考虑模型合并的问题

        • 除了合并模型,第二个方面是使用LOD:随着距离变远模型边数降低(按等级50%衰减),占用额外内存(空间换时间),但是不影响drawcall次数。HLOD(分层LOD)是个高级版本,对一组模型按距离同时优化面数和drawcall次数(同组的模型只调用单个模型的drawcall次数,典型例子是植被系统!)

          • 启用:World Settings -> LODSystem -> Enable Hierarchical LODSystem

          • 可视化:window - Hierarchacal LOD

          • 室外场景往往需要进行距离剔除的同时设置HLOD

       

    • Vertex Shaders(顶点着色器):着色器通常高效且并行运算(super optimized),顶点着色器工作于渲染流程的开始阶段

      • 转换局部坐标到世界坐标

      • 处理顶点的绘制和上色

      • 可以用于世界场景偏移,材质节点中的worldPositionOffset,这种偏移只是视觉上的效果,并不是真的位置偏移,如物理和碰撞盒位置不变(因为是CPU计算的),这一点的典型应用场景:

        • 布料、旗帜等摆动

        • 水材质波动效果

        • 植物随风摆动效果

      • 禁用远距离世界坐标偏移!其他顶点着色器优化策略:

         

     

    光栅化和GBuffer

    • 光栅化(Rasterizing):将渲染信息映射到一个像素网格图像,光栅化过程随着drawcall的逐次调用执行

    • 过度着色:一个像素点只会同时用于显示一个多边形,而由于硬件原因像素着色器每次按2*2个像素进行着色,这会导致实际着色范围要大于本应渲染的区域

      • 可视化:视窗里viewmode - optimizeViewModes - Quad Overdraw

         

        • 可以看到距离越远的物体渲染越慢,因为会有更多的多边形覆盖更小的像素区域(多边形的像素密度增加了),导致过度着色严重

        • 关于过度着色的具体策略:

           

        • 再次提到LOD:除了减少多边形数量,也会优化过度着色

        • 过度着色在前向渲染中影响较大,延迟渲染中影响不大,因此一般也不需太关注

    • Gbuffer概念:引擎的每一帧渲染都会输出不同的images,缓存到Gbuffer,之后的处理都是用Gbuffer中的图像

      • RenderDoc中能直观预览RGBA通道对应的图像:

        R:世界法线贴图(GbufferA)&金属色(GbufferB) G:高光 B:粗糙度

      • 自定义深度:用单独的渲染目标或者说GBuffer缓存模型,然后用于特效等(轮廓特效)

        • 视口 - 高分辨率截图high resulotion screenshot - use custom depth as mask

     

    渲染和纹理

    • textures在导入时会被压缩,不同平台不同,PC使用BC(DXTC),压缩纹理是因为内存和带宽限制,不压缩大小差距巨大,纹理太多或太大会导致渲染卡顿(传输延迟)

    • NoramalMap使用BC5压缩,只保留R/G通道(蓝色的可以计算出来),如果导入的纹理设置不对需要修改

    • 带alpha通道的texture使用BC3, 不带alpha的使用BC1, 禁用压缩的话可以选择RGBA8

       

    • 渐进纹理(MipMaps):纹理随远距离使用更低分辨率的纹理(2次幂衰减),这项技术用于解决噪点问题

       

       

      • window - shadercode - HLSL code可以查看当前材质节点的shader源码

      • 材质编辑依赖引擎大量的shader模板,切换材质域或blendmode导致可用参数变化,本质是切换了shader模板;可以创建自己的shader代码加入项目或插件;每次模板会生成不同用途的着色器版本(材质细节面板的Usage)

    • 像素着色器对性能的影响非常大,分辨率越高材质越复杂影响就越大。如果想概括导致渲染变慢的原因,通常这四个方面:drawcall调用次数,像素着色器,半透明,动态阴影导致了几乎所有的性能问题。

      • 像素着色器复杂度可视化:视窗viewmode - optimization viewmodes- shader complexity (Alt + 8)

    • UE4的材质基于PBR,所有模型材质和着色都是建立在相同的PBR系统上

    • 材质的纹理采样有数量限制:

       

     

    静态光照和静态阴影

    • 静态光照预先计算并存储到光照贴图lightmap

    • 高效但会占用内存,无法动态调整,常用于全局光照烘焙

    • 光照贴图依赖UV布局,因此有UV空间上限限制

    • 光照贴图与底色贴图直接相乘,附加在顶层,类似一种自发光

      uploading.4e448015.gif转存失败重新上传取消uploading.4e448015.gif转存失败重新上传取消

    • 全局光照设置选项:worldsetting - lightmass

      • 烘焙后的光照贴图在lightMass面板的光照贴图属性组下

      • lightmass时一个独立的渲染器,支持网络分布式渲染

      • lightmass烘焙关联设置:

        1、确保场景中的所有模型的UV通道贴图正常(导入时选项:生成uv lightmap)

        2、光照构建质量;3、lightmass面板参数;

        4、Lightmass Importance Volume(体积内的光照质量更高)

    • 间接光照质量:静态光照解决动态模型光照变化的方法,通过ILC存储光照网格点信息,当达到一个网格点时叠加光照到移动模型身上,光照就会发生变化

      • 可视化:show - vasualize - volume lighting samples (需要光照烘焙后可视)

      • 在可移动是模型actor细节面板Indirect Lighting Cache属性设置质量级别

    • 静态光照优化策略:

      uploading.4e448015.gif转存失败重新上传取消uploading.4e448015.gif转存失败重新上传取消

      • 可视化:Viewmode - OptimizationViewmodes - Lightmap Desity

        展示光照UV贴图,用于发现异常扭曲,红色为异常,可以在静态模型的细节面板

        lighting - overriden light map res 调大或调小(动态修改无需重编)

     

    动态光照和动态阴影

    • 四种动态阴影

      • 常规阴影:设定为可移动类型的actor,实时阴影,高对比度而且非常锐利,以至于阴影成像经常过渡

      • 固定光源阴影:设定为固定类型的actor,混合使用全局静态光照烘培的阴影和动态阴影,由于混合了静态会软化成像,阴影效果更加柔和,不会太锐利 (注:静态类型无阴影)

      • 级联阴影贴图CSM:用于室外大型场景的动态阴影过渡,在远距离阴影渐进消失,随摄像机距离变远逐级替换低分辨率阴影贴图。只能用于定向光源,细节面板可以控制级联层数,一种兼容性能的折中方案

      • 距离场阴影:CSM的折中方案在室外远距离渐进消失阴影,远距离无法投射阴影仍然比较突兀。相比CSM,距离场更适用于长距离动态阴影方案,不够精确,但性能损耗很低

    • 点光源与动态阴影:类似反射球捕获,点光源在球形遮罩内计算光源(边缘渐进衰减),当多个点光源范围重叠时会重复计算导致额外开销

    • 动态光源本身性能损耗很小,导致性能问题的不是光照而是动态阴影,因为渲染阴影需要光源到所有几何体的距离信息。阴影由像素着色器完成,所以像素越多开销越大。因为光源离摄像机越近涉及到光源渲染的像素越多,所以开销也越大,因此尽量缩小光源半径并且避免重叠

    • 动态阴影优化策略

      • 不需要的阴影就关闭

      • 多面体面数在几何体渲染时本身影响不大,但是会影响阴影从而影响性能

      • 在室外远距离使用距离场阴影优化

      • 在距离足够远时直接关闭光源和阴影

      • 尽量缩小光源半径并且避免重叠

     

    雾和半透明

    • 半透明是ue中影响性能最大的因素之一,延迟渲染中半透明比较困难,被推迟到渲染最后阶段进行

    • 表面材质尽量使用蒙版混合模式代替半透明混合模式,如果使用半透明尽量设置shading model成无光照,如果非要半透明,则lighting mode设置成表面前向着色

    • 两种距离雾:大气雾指数级高度雾

       

    uploading.4e448015.gif转存失败重新上传取消uploading.4e448015.gif转存失败重新上传取消

     

    后期处理

    渲染末尾阶段使用Gbuffer再度计算产生特效,如泛光light bloom,光束light shaft,景深&背景模糊blur,运动模糊motion blur,颜色分级LUT&颜色校正,曝光explosure

    uploading.4e448015.gif转存失败重新上传取消uploading.4e448015.gif转存失败重新上传取消

     

    [实时渲染补充内容]

    LOD

    • LOD根据物体与摄像机的距离来设置模型多边形的数量,远离摄像机时自动降低分辨率级别来优化性能

    • LOD从0开始,表示原始最高分辨率,屏幕中所占大小比例表示距离远近

    • 模型mesh的细节面板可以手动设置LOD或外部导入模型LOD,也可以使用UE4的LOD组功能自动设置(推荐),设置分组后可查看各级LOD的具体参数,如该级别LOD的屏幕比例阈值

      uploading.4e448015.gif转存失败重新上传取消uploading.4e448015.gif转存失败重新上传取消

    • 确保AutoComputeLODDistance开启,左上角可实时预览LOD参数

    • 室外场景可同时使用LOD和剔除距离

    • HLOD是一种LOD进化版本,不再对单个模型设置,而是通过生成集群和生成代理对一组Actor设置LOD,代理模型就是组合Actor之后的简易模型代替一组模型,从而极大降低多边形

       

    贴花Decals

    • 可以投射在静态网格体或骨骼网格体表面的材质。可以用于血迹弹孔涂鸦高亮等效果。

    • 从 模式(Modes) 面板添加 "延迟贴花(Deferred Decal)"Actor。然后创建材质域为 延迟贴花的新材质。再将该材质应用给贴花Actor。注意轴向投射距离,靠近混合,过远淡出

    • 混合模式Mask,同时使用法线贴图和不透明蒙版产生立体贴花效果

    • 多层贴花使用sort order控制叠加顺序

     

    次表面散射补充

    • 次表面散射(Sub-Surface-Scattering)简称3S,用来描述光线穿过透明/半透明表面时发生散射,光从表面进入物体经过内部散射,然后又通过物体表面的其他顶点出射的光线传递过程。

    • 常应用于蜡烛,大理石,玉石以及皮肤材质

    展开全文
  • 作者简介佳璐,前端开发专家,关注前端框架、性能、质量、效率和新技术。一、背景随着 React Native 在前端业界规模性的应用越来越多,各大厂也对其渲染性能越来越看重。渲染性能的主...

    作者简介

     

    佳璐,前端开发专家,关注前端框架、性能、质量、效率和新技术。

    一、背景

    随着 React Native 在前端业界规模性的应用越来越多,各大厂也对其渲染性能越来越看重。

    渲染性能的主要评判指标是FMP与TTI,在 React Native 以跨平台前端框架身份逐步替代 Native 原生界面的同时,两者的渲染性能对比也逐渐浮出水面。

    同时,渲染性能调优在业内已存在许多可借鉴的经验,而在项目实践的过程中,往往能体验到现实与理想的巨大差距。

    参考业内先行者的经验,针对线上项目做渲染性能优化时,往往会出现事倍功半或不尽人意的情况。

    概括主要原因是以下几个方面存在问题: 

    1)缺少可量化的渲染性能评判标准 

    2)缺少可量化、可视化的优化工具 

    3)简单堆砌式的使用多种优化方式,容易相互抵消优化效果 

    4)优化方式仅局限于前端,忽略了 Native 或 Service 优化方向

    本文将从理论方案、实操经验以及实用工具三个方面介绍携程在 React Native 渲染优化方面的经验,希望能给业内的前端伙伴提供实际的帮助与启发。

    二、评判标准

    评判标准是基于用户交互体感实践出的量化数据,数据达标的界面可做到近似Native 界面交互体感,其中FMP和TTI是渲染优化的两个重要指标。

    2.1 FMP

    用户交互中,“白屏”体感的特征指标。

    “白屏”的原因简单概括如下: 

    • Bundle 包热更新 

    • 启动 React Native 容器 

    • 业务代码加载耗时 

    • 服务请求耗时

    但FMP耗时并非越短越好,若业务逻辑较为复杂,一味地缩短FMP容易造成TTI耗时过久,在整体性能优化上适得其反。

    2.2 TTI

    用户交互中,“直出”体感的特征指标。

    该指标同样经过实践,得出1.6s和1.2s两条符合良好用户体感的数据线。

    • 1.6s是正常发送服务请求界面的基准 

    • 1.2s是采用提前发送服务请求界面的基准

    与FMP不同的是,TTI耗时越短,用户“直出”的体感越好,但也需注意界面功能的完整性(如功能阉割、功能操作延迟较多等情况)。

    三、优化方向

    有了可量化的评判标准后,可以针对性地确认优化方向。

    以携程国际机票列表页为例,如图所示从开始调整至界面渲染结束,主要分为FMP与TTI两个阶段。

    理论上,减少这两个阶段的耗时,就可提升渲染性能。

    下面将从客户端(Native)、前端(React Native)、服务端(Service)三个方向来作详细讲解。

    3.1 客户端(Native)

    3.1.1 React Native 容器

    Hermes 渲染引擎切换

    Facebook 发布的 Hermes 引擎,在实践中的效果优异,iOS与Android端的TTI指标有明显降低。

    有兴趣的同学可看下这篇文章:Hermes引擎分析

    React Native 容器热启动

    当 Native 打开一个崭新的 React Native 界面时,需要经过如下步骤:

    其中启动 React Native 容器至加载业务代码所消耗的时长是FMP指标的关键因素。

    而容器热启动的意义在于将界面加载过程中的必经流程提前运行,加快界面渲染的速度。

    通常,当有多个界面采用流式加载的方式时,再前一个界面调用 Native API 提前启动下一个界面所需的 React Native 容器。

    React Native 容器复用

    当多个界面采用流式加载,往往会存在ABAB 式的用户流水。

    由于A界面在打开B界面的时候,是作为一个容器被B界面遮罩,并没有被关闭,所以A界面只被打开了一次;而B界面在这用户流水过程中实际被打开了两次,即返回A界面时,B界面的容器就被销毁了,同时其中的 React Native 容器也被销毁。

    基于上述场景,可以发现优化点在于容器及其中的 React Native 容器内容可以被缓存,便于下一次进入时可以被复用。

    这里需要注意两个风险点:

    1)过多的容器及其中的 React Native 容器内容被缓存时,容易造成内存溢出,从而引起 App Crash;

    2)复用 React Native 容器内容时,会保持上一次会话的全局变量,容易造成业务逻辑错误。

    3.1.2 Bundle

    Bundle 预下载

    在非业务型界面中,提前下载之后可能会被打开的业务界面 Bundle(若有更新增量存在)。

    该方案仅对 Bundle 提前进行下载操作,并不会进行增量文件加压及更新。

    使用该方案后,会面临如下的问题:

    1)App中的界面数量往往比较多,全部使用 Bundle 预下载可能会造成网络下载列队阻塞,影响正常使用。

    2)Bundle 存在下载失败的概率,会丧失预下载的想要达到的效果。

    针对上述可能出现的问题,需要进行深度优化:

    1)预下载的时机需要符合如下几个条件:

    • 利用底包优势,以 Native 实现的界面

    • 业务改动频率较低

    • 具备一定停留度的界面

    2)采取优先级异步多线程下载策略,按不同维度设定优先级,如 Bundle 使用率。

    3)重试机制,类似 setInterval 轮询增量更新列表

    Bundle预加载

    在 React Native 容器热启动之前,解压 Bundle 文件并更新。

    通常配合 React Native 容器热启动和 Bundle 预下载使用。

    Bundle 的加载完成了下述3件工作:

    1)更新Bundle文件 

    2)编译JS代码 

    3)执行JS代码

    随着 React Native 容器采用 Hermes 引擎,Bundle 被打包为单个文件,相比使用 JSCore 被打包成多个文件来看: 

    1)更新 Bundle 文件阶段,单文件的更新速率优于多文件 

    2)编译JS代码阶段,单文件减少了多个文件加载耗时

    3.1.3 Native To React Native API

    Sync 同步

    React Native 与 Native 之间采用异步通信机制,当线程繁忙时,会产生阻塞和等待。

    另,在首屏渲染过程中,内存获取数据比较慢的场景也会出现,耗时可能高达200ms。

    解决上述问题,主要有以下几个方向:

    • 对内存读写数据类 API 

    • Sync API 耗时可控在毫秒级 

    • Chrome Dev 不支持 Sync,需特殊处理 

    • 有利于解决阻塞依赖 Native 异步接口调用的场景

    此时,使用 Sync 同步方案显得可行,可解决如下场景:

    • 获取 ABTesting 实验号 

    • 获取本地 Storage 内容 

    • 获取功能开关列表 

    • 获取屏幕 Size 

    • SOTPCookie

    3.2 前端(React Native)

    3.2.1 Bundle 瘦身

    Bundle 中存在几种文件类型,针对不同类型选择不同的优化方案:

    • 代码字符串 

    • Iconfont 字符串 

    • 图片文件

    代码字符串

    冗余代码是代码 Size 的主要问题,而冗余代码的产生主要源自于四个方面:

    • 已下线的需求代码 

    • 已结项的实验代码 

    • NPM 冗余调用 

    • 缺乏抽象的重复代码

    解决方案:

    • 整理已下线需求,删除相应代码及库文件 

    • 使用组件库及方法库,减少重复代码 

    • 抽象可复用的组件,使用高阶组件

    图片文件

    在打包压缩过程中,图片文件的压缩比极低,越大的图片占用的 Bundle Size 越大。

    解决方案:

    • 较大的图片在保证清晰度的前提下压缩后打包 

    • 视业务场景使用网络图片替代 

    • 较小图片可以使用 IconFont 替代

    3.2.2 模块

    LazyRequire

    在编译过程中,import会被编译成 require,require 所完成的功能是读取JavaScript 模块并执行。

    而大模块的执行会耗费较多时间,使得界面加载速度变慢。

    因此,优化的方向是当模块被需要才加载。但 React Native 提供的标准 require 目前并不支持动态加载。

    需要修改 React Native 源码的打包功能,使其支持动态加载功能,并提供出对应的 API 来供业务方实现。

    使用示例如下:

    import {lazyRequire} from 'react';
    let moduleA = lazyRequire('../src/ModuleA');
    

    动态加载

    使用 import 语句导入模块时,会自动执行所加载的模块。而当使用组件库或公共方法库的时候,往往并不希望如此。

    假设 Common.js 文件为公共方法库

    import A from './A';
    import B from './B';
    import C from './C';
    export {
        A,
        B,
        C
    }
    

    此时若希望只引用 Common.js 中的A模块,即

    import {A} from './Common.js';
    

    但实际B和C模块代码也被执行了。

    为了使程序能如你所愿的仅执行A模块,需要使用属性 getter 动态 require 的方式来修改 Common.js 文件。

    const Common = {
        get A(){
            const module = require('./A');
            return (module && module.__esModule)? module.default:module;
        }
        get B(){
            const module = require('./B');
            return (module && module.__esModule)? module.default:module;
        }
        get C(){
            const module = require('./C');
            return (module && module.__esModule)? module.default:module;
        }
    }
    module.exports = Common;
    

    这样在使用到A模块的时候才会执行 require('./A').default,并不会加载B和C。

    至此,使用该方式导出模块可以减少引用模块时的无效加载数量,达到优化渲染速度的目的。

    3.2.3 渲染方式

    骨架屏/呼吸态

    骨架屏是有效减少用户体感“白屏”的有效措施,通常在骨架屏完成耗时较长的关键性任务,如核心服务请求、重要异步回调等。

    同时,骨架屏也是缩短 FMP 标准的重要方法,主要方式:

    • 减少加载骨架屏之前的非必要模块引用

    • 核心服务请求参数的拼接可放在骨架屏渲染之前完成

    • 骨架屏自身的渲染结构足够简单

    分批次渲染

    分批次的概念主要运用在列表型界面或内容型界面。

    顾名思义,是将界面需展示的内容,分成不同阶段/批次进行渲染,阶段/批次的数量根据业务自身情况而定,往往以覆盖满屏幕的主要区域为宜。

    该方案对提升TTI有较大作用,可数量级的减少渲染内容,从而降低渲染耗时。

    渐进式渲染

    React Native 渲染的本质是将 JSX 构建的虚拟 DOM 树通过 Native Render 的方式绘制界面内容。

    虚拟 DOM 树结构越复杂,Native Render 所需绘制的时间也越长。

    从这个特性出发,可以通过降低虚拟 DOM 树结构的复杂度来减少渲染耗时,用尽可能短的时间到达 TTI 阶段。

    降低虚拟 DOM 树结构复杂度的底线是最低程度得保证业务功能的完整性,而在其渲染完成后(达到TTI阶段),通过 setState 去更新渲染完整的虚拟 DOM 树结构即可。

    下面两幅图在渲染过程中采用了渐进式渲染,可观察航空公司部分:

    延迟渲染

    界面在相对复杂的情况下,渲染的模块会比较多,渲染的耗时也会随着需要渲染的模块数水涨船高。

    对待渲染的模块区分核心和非核心,或者区分模块需渲染的轻重缓急,优先渲染核心/重要的模块,符合界面基本交互功能(达到TTI阶段),再渲染非核心/次要模块来完成整个界面的渲染工作。

    按需渲染

    界面中不可避免的会存在一些浮层或者二级界面,下面统称为次级界面。

    这次次级界面在TTI阶段前,大部分是不需要进行渲染的,可以配合 LazyRequire 的方式完成。

    预渲染

    空间换时间的经典方案。

    假设存在流式界面A -> B,若能在A界面时能够提前渲染B界面的话,理论上可以做到在打开B界面时做到“直出”效果。

    若要做到上述方案,需要结合多个优化方案,这里只分析“预”的实现方式。

    在A界面时,通过 Native API 热启动一个新的 React Native 容器,同时在新容器内预加载B界面的 Bundle 并执行。

    当从A界面进入B界面时,由于B界面已经完成/正在渲染,B界面可达到“直出”效果。

    优化结构

    虚拟 DOM 树的结构越复杂,所需消耗的渲染时长也就越久,也就越晚到达 TTI 阶段。

    首先,通过工具去观察虚拟 DOM 树结构的深度和广度,使用渐进式渲染方案减少深度,同时也使用分批次渲染方案减少广度。

    其次,由于研发过程属于 TeamWork,一个结构合理的 UI 组件库可以大幅减少优化结构所需的工作量。

    3.3 Service

    优先发送服务请求

    从进入界面到渲染界面完成(即TTI阶段)需要经过许多代码逻辑阶段,研发人员需要理清这些逻辑阶段的依赖关系,并做出优先级的策略。

    为了更快的将服务请求发送出去,利用等待服务返回数据的时间差去运行其他渲染所需的逻辑,待服务数据返回后再去渲染界面。

    但需要注意的是,若服务返回时间较长,可能会子执行完其他逻辑时进入 render阶段,当服务返回数据后再次 render,造成 TTI 阶段耗时有所延长。

    解决方案是采用服务预搜索后,使用同步请求服务数据的方式来避免重复/无效 render。

    按需异步获取数据

    类似按需渲染的场景,同一个界面需要请求的服务个数往往不止一个,除了渲染界面主要模块所必须的核心服务外,其他次要模块的服务请求可以放在 TTI 阶段后请求。

    图中红色部分的模块,在渲染的界面中并不属于核心模块,可以采取延迟按需请求的方式获取数据后再进行渲染。

    服务预搜索

    除静态界面外,几乎所有 CSR 界面都需要在渲染过程中发送服务请求,再根据服务请求返回的内容渲染界面。

    等待服务请求响应的时长将直接拖慢到达 TTI 阶段的耗时,而提前发送服务请求是否可行?

    前端在发送服务请求前往往需要拼接较多的请求参数,这些参数中存在很多变量,而变量的来源有许多是来自于用户交互。

    正因为这样的场景较多,提前发送服务请求的难度也陡然上升。同时,也会给服务端带来请求数量成倍增加的副作用。

    处理的方式有以下几种,可根据业务形态的不同进行选择或组合:

    • 区分业务不同场景,针对大部分场景做提前请求服务的操作 

    • 需要依据多个用户交互结果作为请求参数的场景,可配合 BI 用户模型做到较精准的提前请求

    在提前发送请求服务后,在进入下一界面时,代码逻辑仍然会正常发送服务请求,这里需要做好网络缓存。具体操作方式如下:

    • 请求服务时,根据请求的 url 和参数通过 Hash 生成一个唯一的 Key 

    • 请求返回时,将返回的数据存入本地 

    • 在一定时间内,发送相同 url 和参数的请求,都会匹配已生成的 Key,从本地拿取数据返回,而不进行真实的网络请求

    四、实践工具

    每个项目/界面的业务逻辑不同,从而代码逻辑也不相同。显然在优化不同界面时,采用的优化方案也不同。

    那么,在优化界面过程中该如何选取适合的优化方案,显得尤为重要,而这个过程中,经验并不能起到决定性的作用。

    需要借助线上和线下两方面的工具来完成性能分析工作后,再依据经验选择合适的优化方案。

    4.1 Offline

    借助 Chrome Devtool Performance,可以分析运行时的性能表现,主要借助以下两种方法采样性能数据。

    • 调试环境:通用的 Web 性能分析方案,打开 React Native 调试功能-->运行项目-->采样数据。

    • 真机环境:在测试环境中修改 React Native 代码,模拟 Profile 数据结构生成埋点数据。

    以上两种方法存在部分差异:

    • 调试环境:采样数据来自于模拟器,数据的真实性存在偏差,多用于快速试验优化方案效果。

    • 真机环境:采样数据来自于真实机型,数据的真实性较为可靠,多用于验证优化方案效果,以及针对特殊机型验证优化效果。

    两种方式采样到的性能数据,分为 Timing 和 Console 两种。

    4.1.1 Timing

    作用是分析视图组件渲染顺序与耗时,如下图使用 Timing 火焰图,在视图渲染层面分析性能:

    • 组件渲染顺序与耗时:“火焰模块”的长度标识组件渲染耗时(包括其子组件),至上而下可以分析组件的渲染顺序。

    • 组件间渲染空闲时间:通过两个“火焰模块”之间结构,分析各模块组件之间的渲染顺序,其中空白部分表示组件渲染空闲的耗时。

    4.1.2 Console

    作用是通过代码执行层面分析性能,如下图使用 Console 时序图分析性能:

    • 代码块执行耗时:单个模块表示该代码块执行的总耗时,可以更直观的分析代码块的执行顺序。

    • 异步与同步代码之间的关系:简单的埋点计时并不能精确计算异步耗时,而模块与模块之间的叠加关系,可以直观分析异步代码的执行耗时。

    4.2 Online

    业务代码在上线后,存在许多环境因素,如网络情况、机型覆盖面、OS系统、Bundle 更新及解压等。

    性能优化的目的是让用户切实提升使用 App 的感受,线上性能数据采样就成了重要的试金石。

    线上性能数据采样主要记录的是界面渲染的 TTI 和 FMP 耗时点,采样的方式主要采用屏幕像素检测,检测用户访问的界面屏幕渲染出像素点的耗时。

    采集到 FMP 和 TTI 数据之后,根据 App 版本、OS 系统类型、时间周期等维度进行拆解,绘制出对应的性能波形图和P90覆盖情况。

    具体可见文章《携程无线APM升级实践》

    五、小结

    渲染性能已然成为大前端工程师必须面对的一道课题。本文通过工具及方案的介绍,将前端优化的视野打开,更加系统得看待渲染性能问题。

    叠加使用各种优化方案在优化渲染性能方面具备一定的普适性,部分优化理论同样也适用于 H5 与 Native 平台。

    【推荐阅读】

    《携程架构实践》

    京东

    当当

    《携程人工智能实践》

    京东

    当当

     “携程技术”公众号

      分享,交流,成长

    展开全文
  • 文章目录关键路径渲染优化什么是优化关键路径关键指标构建对象简单流程图代码准备不设置明确禁止缓存private与public缓存过期策略1、三种方式设置服务器告知浏览器缓存过期时间2、两种方式校验资源过期强制校验缓存...
  • Egret性能优化优化渲染

    千次阅读 2018-07-02 15:14:37
    其实大部分时候都是由于开发者所使用的方法不正确。导致游戏性能下降,甚至出现卡顿的现象。游戏作为性能消耗大户,很多时候都会...你需要对引擎渲染部分非常了解。能够在开发中避免不必要的低级错误。这篇文章简单...
  • 做图用什么显卡好,图形渲染CPU重要还是显卡重要发布时间:2018-09-15 09:19来源:互联网当前栏目:电脑教程群里有朋友向笔者咨询过关于专业图形设计的电脑配置,其实对于图形设计来说主要是在cpu和显卡的选择方面,...
  • Vue单页面应用优化SEO之预渲染

    千次阅读 2018-12-06 14:06:50
    这里优化SEO重点说vue-cli框架,毕竟是spa项目,所以SEO十分不友好,原因在于它自身并不是静态html,而是由js动态生成的(通过文档碎片劫持的方式以及Object.defineProperties等方法,有兴趣可以去研究研究vue的原理)...
  • 在Vue中可以通过{{变量}}或者在里面写表达式,但是在html中为什么不行呢?在一个篇章中讲过 从:模板---------------->渲染函数------------------>vnode-------------------->视图。 编译模板的主要目的...
  • vue 多级嵌套数组渲染性能优化

    千次阅读 2020-05-12 15:46:06
    最近遇见一个问题,移动端一个页面展示折叠展示数据,具体要展示多个区域,每个区域下面有不同业务人员,每个业务人员有自己的客户,每个客户又有不同的记录,即这个页面要渲染一个4层的数组。具体页面展示类似为: ...
  • 本节主要讲一下虚幻渲染之前,都发生了什么渲染流程:首先,虚幻的渲染由三个线程共同完成。分别是CPU线程,DRAW线程,和GPU线程。CPU线程:顾名思义,运行在CPU上,用于计算游戏中...
  • 4.实战解决过渡优化 CPU与GPU工作流程 CPU和GPU是什么?CPU简单来说就是说逻辑计算,系统管理,比如计算图片的大小宽度,显示操作等等,在没有GPU的时代,不能显示复杂的图形,运算速度也远跟不上复杂的三维游戏...
  • RPA是干什么的?

    万次阅读 2020-09-18 12:56:06
    一句话让你明白这个技术是什么,一种模拟电脑鼠标键盘操作且可以代替人进行重复性、规则化电脑端操作的技术,展开想象会发现能够应用在公司各个部门各个业务线,简单整理如下: 感觉还是很迷糊? 钉钉跟客户发...
  • 主线程MainThread与渲染线程RenderThread

    千次阅读 2020-12-28 23:03:25
    这里我们首先要明确什么是硬件加速渲染,其实就是通过GPU来进行渲染。GPU作为一个硬件,用户空间是不可以直接使用的,它是由GPU厂商按照Open GL规范实现的驱动间接进行使用的。也就是说,如果一个设备支持GPU硬件...
  • VR画面渲染性能是这样提升的

    千次阅读 2020-07-16 15:25:36
    本文您将了解到: 1,VR渲染面临什么问题? 2,如何做好VR的渲染?...3,怎样提升VR渲染的性能?...渲染对于VR内容的开发来说,是非常重要...包括渲染方面的基础知识,以及在Unity里面如何去优化渲染的效率。 废话不多.
  • 浏览器内核渲染原理及优化

    千次阅读 2016-11-30 16:27:10
    一、简单介绍一下什么是浏览器内核。浏览器最重要或者说核心的部分是“Rendering Engine”,可大概译为“解释引擎”,不过我们一般习惯将之称为“浏览器内核”。负责对网页语法的解释(如HTML、JavaScript)并渲染...
  • NeRF:深度学习完成3D渲染任务的蹿红

    千次阅读 多人点赞 2021-07-18 19:14:06
    NeRF:神经网络完成3D渲染任务的蹿红1 引言2 前NeRF时代2.1 传统图形学的渲染2.2 神经网络侵略3D渲染任务:NeRF呼之欲出隐式场景表示(implicit scene representation)DeepSDF3 NeRF!3.1 Radiance Fields(RF)...
  • 流体效果相信大家都不陌生,实现方式中的一种是将粒子渲染成 metaball。什么是metaball metaball就是粒子加上其周围的 密度场(density field)。两...
  • 服务端渲染(SSR)

    2020-07-19 00:28:59
    什么是服务端渲染?(服务端渲染的运行机制) 为什么使用服务端渲染?服务端渲染解决了什么问题? 什么情况下使用服务端渲染?(服务端渲染的应用实例与使用场景) 一、概念 首先,说到服务端渲染我们要先对...
  • [学习笔记]Egret性能优化优化渲染

    千次阅读 2018-05-31 09:47:50
    其实大部分时候都是由于开发者所使用的方法不正确。导致游戏性能下降,甚至出现卡顿的现象。游戏作为性能消耗大户,很多时候...你需要对引擎渲染部分非常了解。能够在开发中避免不必要的低级错误。这篇文章简单的...
  • 最近在研究页面渲染及web动画的性能问题,以及拜读《CSS...主要内容包括了为何需要优化滚动事件,滚动与页面渲染的关系,节流与防抖,pointer-events:none 优化滚动。因为本文涉及了很多很多基础,可以对照上面的知...
  • 顺便回忆了下现在的几种渲染模式: SSR (Server Side Rendering) SSG (Static Site Generation) SSR With hydration CSR with Pre-rendering CSR (Client Side Rendering) Trisomorphic Rendering 都不是什么新鲜...
  • Unity引擎渲染模块知识Tree

    千次阅读 2019-11-21 10:18:40
    渲染效果是游戏表现力的核心卖点之一。尤其近几年随着引擎技术的革命翻新,硬件技术的突飞猛进,以及玩家愈发挑剔的要求,游戏的表现力正在进入到崭新的层面。有哪些主流的Shader、自阴影、后处理技术?如何加强人物...
  • 麒麟子收到了许多研发负责人的反馈,说自己的技术团队开发效率一流,已经开发了不少2D/3D游戏。但遇上渲染优化、性能优化、内存优化、兼容性处理等项目后期问题的时候,就束手无策了。
  • iOS之性能优化·UITableView深度优化

    万次阅读 2021-10-23 16:20:59
    UITableView 的优化主要从四个方面入手: 提前计算并缓存好高度(布局),因为 tableView:heightForRowAtIndexPath: 是调用最频繁的方法; 滑动时按需加载,防止卡顿。这个在大量图片展示,网络加载的时候很...
  • 以3ds Max的图形制作用电脑为例,其作用包括三个阶段:第一阶段是建立模型,第二阶段是光源材质,第三阶段是渲染。这三个阶段对工作站的子系统的要求侧重点各不相同,在3ds Max里面,对硬件的要求也主要集中在这三个...
  • OSG 高效渲染 之 多线程模型与渲染

    千次阅读 2019-10-28 09:27:54
    这里以OSG多线程渲染为例,谈谈OSG渲染引擎多线程渲染模式,再说说哪里有点“过时”了需要改进。 先谈点题外话,曾经看到知乎上一篇关于游戏引擎的设计讨论的文章,有位“大大”提出游戏引擎的设计关键在于架构设计...
  • 本文您将了解到: 1,VR渲染面临什么问题? ...2,如何做好VR的渲染?...3,怎样提升VR渲染的性能?...渲染对于VR内容的开发来说,是非常重要的议题。...包括渲染方面的基础知识,以及在Unity里面如何去优化渲染的效
  • Android性能优化之UI渲染优化

    千次阅读 2016-09-08 15:45:21
    性能优化都需要有一个目标,UI的性能优化也是一样。你可能会觉得“我的app加载很快”很重要,但我们还需要了解终端用户的期望,是否可以去量化这些期望呢?我们可以从人机交互心理学的角度来考虑这个问题。研究表明...
  • 因此,如果需要频繁切换的情景下, hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好。 图片处理 1.大图片也会造成页面切换的卡顿 有一部分小程序会在页面中引用大图片,在页面后退切换中会出现掉帧卡顿的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 20,264
精华内容 8,105
关键字:

渲染优化是干什么用的