整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

什么是flash什么是html5?

今,HTML5 可谓如众星捧月一般,受到许多业内巨头的青睐。且不说谷歌、苹果等业内巨头把它描绘为互联网体验的未来,即便是以不服从标准著称的微软,也向它频频示好, 决心在 Internet Explorer 9 中加入对 HTML5 的大量支持。然而,HTML5的路途真的将一帆风顺么?本文将列举了一些在HTML5发展和普及过程中需要解决的问题。

   什么是HTML5?

   根据维基百科上的解释,HTML5 的前身是 Web Applications 1.0,由 WHATWG 于2004年提出。2007年,它被 W3C 接纳,并于2008年1月22日发布第一份正式草案。

   以下摘录自维基百科的文字介绍了 HTML5 的特点和与 HTML4 的差别:

新应用程序接口(API)
即时二维绘图
定时媒体播放
储存
离线
编辑
拖放
通讯/网络
后退按钮管理
MIME 和协议处理程序时表头登记

   与 HTML 4 的不同之处

新的解析顺序
新的元素:section, video, progress, nav, meter, time, aside, canvas
input 元素的新属性:日期和时间,email, url
新的通用属性:ping, charset, async
全域属性:id, tabindex, repeat
移除元素:center, font, u, strike, s, frameset, frame, applet

   战胜 Flash, HTML5 还需要什么?

   决定胜负的因素很多,在此分为两部分分析。

   一、技术方面

   HTML5 与 Flash 在功能上并不是完全重叠的,比如对于摄像头等计算机硬件的调用,仍然只能使用 Flash 或其他方法实现。但是 HTML5 却引入了一些让 Flash 不得不认真对待的元素。其中最为人所知的和最重要的即为 canvas 和 video 标签。在我看来,其他的新属性跟 Flash 的竞争关系很弱,只有此二者是真真正正要抢 Flash 的饭碗,而且一旦普及,将深刻的改变整个互联网,可谓是 HTML5 的左膀右臂。

   canvas

   在 Flash 流行之前,曾经出现过很多种在网页中实现绘图功能的方法,其中包括著名的Java Applet 。这些方法各有千秋,相互竞争不休。但是在 Macromedia 公司推出 Flash 之后,这场战争很快就结束了。

   为了对抗 Flash,又有很多新的技术被提出,其中就包括现在 Flash 的东家 Adobe 提出的 SVG。然而这没有能够阻止 Flash 迅速地被网民接受。基于 Flash 的动画、游戏等应用几乎是在一夜之间蔓延到了互联网的每个角落。

   而今,新的挑战者出现了,他就是 HTML5 的新标签 canvas。

   canvas 相比 Flash 显然是有其优点的。它不依赖于外部插件、与浏览器渲染引擎紧密结合、节约资源,最重要的是极大的简化了图形和网页中其他元素的交互过程。

   对于 Flash 来说,是 Flash中的元素与网页中其他元素进行交互是要消耗大量时间和资源的,另外在编程上也相当不方便。

而 canvas 本身就是 HTML5 的一个元素,可以像操作普通 HTML 元素一样操作它。开发人员可以将所有的代码整齐地写在一个文件里,降低了维护与更新的难度。

   然而 canvas 也有其缺点:

   其一,开发者不得不编程描绘每一个点和矢量曲线,在旋转缩放时更需要和矩阵变换打交道,这会增加描绘复杂图形的难度。而在 Flash 里,图形显示的 API 被封装在名为“Sprite(显示列表)”的类里,大部分图形元素都继承于此类,开发者可以使用多种工具设计图形,对图形进行旋转放缩只需要简单的调用类 的函数。

   其二,动画的实现存在缺憾。canvas 虽然提供了不同于传统的通过 div 块实现动画的方法,但这种方法仍然非常繁琐。开发者必须在每一帧动画显示时清空画布,然后重画所有元素,这必然导致包含大量元素的场景动画缓慢,只移动少 量元素就要重画整个画布会浪费大量资源。而且创建动画也是一件十分繁琐的事情。相比起来,Flash的实现就方便多了,虽然从最底层来说,动画时仍然需要 重画整个画布,但其被交予 Flash Player 自动处理,无需开发者手动管理。基于字节码的 Flash 在解析的过程中将会比即时编译的 HTML5 和 JavaScript 快速。一般来说,复杂动画将会更流畅。另外,良好封装的图形类和强大的设计工具使得动画的创建非常方便。

   其三,没有提供一套方便的事件体系。开发者也许需要通过捕获鼠标在 canvas 中点击的坐标,判断用户到底点击了什么图形元素。在这个过程中可能要遍历所有的显示元素并判定点是否在图形内,实现起来比较繁琐,更不要说实现事件的冒泡 和递归模型了。虽然今后出现的图形库可以解决这个问题,但这实质上相当于使用 JavaScript 构建了一套事件响应模型,其效率显然不如内建于浏览器的原生事件模型高。在 Flash 中,事件也被良好封装为类,捕获点击等事件自然不在话下,更重要的是提供了判断两个图形是否有交集的事件和函数,这在游戏编程中非常方便。另 外,Flash 的最新版本将会支持多点触摸事件的响应,而 HTML 想要支持这点恐怕要等到 HTML6 了。

   由以上分析我们可以看出,HTML5 需要的几个非常重要的东西:一个强大易用的图形库、硬件加速的图形解析和重绘、一个强大的编 机器(IDE)

   目前已经出现了基于 canvas 实现的游戏引擎。但是从效果上看仍然无法与 Flash 媲美。

WebGL 的提出让我们看到了硬件加速的希望,这将极大的改进图形显示的速度。但是目前它只被少数开发版本的浏览器支持。

   IDE 方面,讽刺的是恰恰是 Adobe 为 Adobe Flash CS5 添加了一个将 Flash 转化为 canvas 的功能。在 JavaScript 方面,鉴于其为非强制类型的编程语言,对其进行代码提示等非常困难,提高编程效率较难。

   如果以上三个问题不能被良好解决,将会限制 canvas 所能实现的效果的丰富度,增加开发的复杂度,从而最终阻碍其普及。

   图为一个用 canvas 实现的绘图应用

   video

   video 标签可能是 Adobe 最反对的东西了,它极有可能打破 Flash 在在线视频领域的垄断地位。

   但目前的情况是作为 video 内容的视频存在编码问题,Apple 和微软所支持的 不是开放标准,浏览器厂商必须为其付费。因此,作为三大浏览器之一的火狐浏览器拒绝支持此编码格式。谷歌虽然也收购了一套优质的编码技术,但是目前没有迹 象表明谷歌会开放这个技术标准。

   根据最近的统计,虽然 Google Chrome 浏览器和 Apple Safari 浏览器增长很快,但浏览器市场还是主要被火狐和 IE 所统治。如果火狐坚持不支持 编码格式,video 标签的推广将会十分困难。

   所以,HTML5 需要一个既开放又优质的视频编码标准

   图为 video 标签 的演示

   二、商业方面

   团结

   要让微软、谷歌、苹果这三个在很多方面存在竞争关系的业界巨头团结一心地支持同一套标准是很困难的。

   苹果方面对 Flash 痛下杀手,微软方面则极少参与这场论战。至于谷歌则在支持 HTML5 的同时在 Android 中加入了 Flash 支持,甚至存在将 Flash 纳入 Chorme 安全沙箱的计划。在这种情况下,如果 Adobe 能够良好利用三大巨头之间的分歧并加以运作,HTML5 的前景堪忧。

   即便 Adobe 没有那样的智慧与能力挑拨三大巨头之间的关系,三大巨头自己就可能葬送 HTML5 的未来。前车之鉴就是大名鼎鼎的 OpenGL。这一标准成立之初的联盟成员几乎可以用豪华来形容,结果因为各个成员之间为了自己的利益相互争吵,使得OpenGL的发展速度远不及 Direct3D,直至到目前这样游戏市场几乎被竞争对手占据、应用范围局限在专业领域的情况。

   用户的接受

   无论各大厂商如何宣传,用户的接受才是最后的检验标准。目前来看 HTML5 在普通桌面领域可能的作为不大,与 Flash 的关系必然是长期并存。原因在于用户并不在意页面到底使用的是什么技术,而更关心最后的效果怎么样。HTML5 的 canvas 若要达到 Flash 实现的相同效果所需要的难度更大,这样限制了中小网站在网页里使用 canvas 的积极性,如果 canvas 不能普及,就相当于 HTML5 断了一条腿,而 video 标签的编码问题再得不到解决,HTML5 真的就没办法和 Flash 竞争了。

   大胆的预测

   在最后,我将对 HTML5 和 Flash 的这场世纪之战做出我自己的预测。

   首先用一个比喻描述目前的情况:

   谷歌、微软、苹果、Adobe 四家围在一起打牌,其中 Adobe 是庄家,手中的牌最多最好。苹果、微软都很想把 Adobe 从庄家的位置上拖下来,但是又不愿意合作。谷歌与 Adobe 关系暧昧,但是也有自己的打算。

   在这场牌局中,Adobe 几乎是立于不败之地的,从目前来看 Flash 被 HTML5 完全取代的可能不大,原因在于Flash 已经占领了绝大部分传统桌面终端的市场,其地位几乎无法撼动,即便 Flash 做得不够好,但是只要没到很不好的地步,还是无法被超越。虽然传统桌面收到了新兴的移动终端的挑战,但是这一过程将发展得比 Flash 和 HTML5 之间的竞争更缓慢。Adobe 控制着 Photoshop、Dreamweaver 和 Fireworks 等知名软件,制作网页即便可以缺少 Flash 却无法缺少    Photoshop,即便是编制全 HTML5 的网页,Dreamweaver 依然是首选的网页制作利器,制作 canvas 也可以使用 Flash CS5 新加入的生成 canvas 的功能。

   图为 Adobe Flash CS5

   广受诟病的 Flash Player 并不能给 Adobe 带来直接的利润,它的意义在于将富媒体应用的市场和标准掌握在手中。用于制作 Flash 的编辑器才是 Adobe 真正的利润来源。如果在 HTML5 的时代,开发者仍然不得不选择 Adobe 的产品来制作基于 canvas 的交互页面,那么又何必需要 Flash Player 的存在呢?Adobe 更是省下了维护一个复杂系统的费用。

   至于微软,他内心是非常纠结的,他手里有 Internet Explorer 这张不知道是好是坏的牌,原因在于由 Internet Explorer 6 占领的浏览器市场份额仍然没有被有效释放,新版本的 Internet Explorer 不得不跟自己的前辈竞争。另外,微软也急于推广自己的 SliverLight ,这一产品与 Flash 和 HTML5 都是竞争关系。支持 HTML5 或多或少会对 SliverLight 的推广有所打击。所以微软必然不会全力支持 HTML5,而是只将它作为 SliverLight 的补充。

   苹果的算盘打得很响,他要从移动终端领域着手,逐步渗透到桌面领域。iPhone 是第一步,也是相当成功的一步,它的存在说明智能手机领域并不需要 Flash 的存在,事实上,Flash 在这一领域表现的确很差。但是真正关键的是 iPad。iPad无论是屏幕大小还是操作体验都更接近与普通桌面电脑,如果平板电脑被证明不需要 Flash 的存在,那么桌面电脑为什么不可以?如果大量用户通过使用平板电脑而习惯了没有 Flash 的互联网体验,那么 Flash 就真的没有未来了。iPad发售之后 Adobe 与苹果之间争论的升级,从侧面证实苹果和 Adobe 都看到了平板电脑将在这场战争中发挥的作用。

   但是苹果真的能如愿以偿么?苹果的产品即便销售很好,也不可能做到像微软的产品这样普及。原因在于苹果的产品文化就在于提供高质量和高品位的体验, 而这种体验伴随着高价。必然只有少数人能够用得起苹果,必然只有使用苹果是能够成为一种身份和品味的象征,苹果的产品才会有这么大的吸引力。难道除了苹果 就没有其他厂商可以提供同样的技术了么?难道微软不能像苹果一样以用户体验为先么?显然不是,重要的原因在于微软所要提供的是一个给所有人使用的产品,这 一产品要有广泛性,要有继承性,而且不能太昂贵。所以微软在用户体验方面改变的动力不足,微软试图在 Visita 里极大的改变用户体验的方式,原来使用 XP 非常熟练的用户到了 Visita 里就变得不知所措了。结果显而易见。

   事实上,苹果从一开始就不打算让所有人都用上苹果,只要有少部分人愿意掏钱购买,它就能赚足够的钱。

   到了平板电脑这里也是一样,iPad 不可能独自积累到足够大的用户群,以至于可以挑战传统桌面终端。更具性价比的其他厂商的产品将会让更多人享受到平板电脑。而且这些平板电脑很可能会支持 Flash。这样的话,利用新兴终端,改变用户对 Flash 的依赖的计划就会失败。

   谷歌方面,这场战争的胜负对谷歌的影响都不大,只要能把用户留在页面上,它并不在意到底使用的是 HTML5 还是 Flash。然而他却最终胜负有着非同寻常的影响力。谷歌除了 YouTube 之外,很少在自己的产品中使用 Flash。也许谷歌认为一个开放的标准更容易控制。对于 Flash 这样封闭的产品,虽然好用,但是谷歌很难对其发展方向有发言权。而今,Chrome 浏览器的迅猛发展日益增加了谷歌在 HTML 新标准中的发言权。但是这并不意味着谷歌会完全抛弃 Flash。

   Flash 可以作为谷歌牵制苹果的重要工具,作为 Android 挑战 iPhone 的筹码。谷歌正筹划将 Flash 纳入 Chrome OS 的安全沙箱。如果成功,Flash 饱受诟病的耗电问题、安全问题都能得到较好的解决。

   由此得到结论,Flash 气数未尽,仍将长期统治互联网富媒体领域的市场。在移动领域的发展将取决于谷歌的态度,但可以预见的是将会有很多困难。

   至于 HTML5,预计在1-3年内会达到相对普及的程度,但是不会取代 Flash。他们之间甚至是可以和谐相处,取长补短的。然而不要对 HTML5 的发展速度抱有太大希望,毕竟业内巨头之间矛盾重重,现有标准能够被各大浏览器无差别支持就已经相当困难了,想要加入任何新功能都要很久才会被广泛支持。 这样缓慢的发展速度如何能够体现出强大的竞争力,我们拭目以待。

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是 JS 引擎。渲染引擎在不同的浏览器中也不是都相同的。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。

本文我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。

想阅读更多优质文章请猛戳GitHub 博客。

页面加载过程

在介绍浏览器渲染过程之前,我们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。

要点如下:

  • 浏览器根据 DNS 服务器得到域名的 IP 地址;
  • 向这个 IP 的机器发送 HTTP 请求;
  • 服务器收到、处理并返回 HTTP 请求;
  • 浏览器得到返回内容。

例如在浏览器输入https://juejin.im/timeline,然后经过 DNS 解析,juejin.im对应的 IP 是36.248.217.149(不同时间、地点对应的 IP 可能会不同)。然后浏览器向该 IP 发送 HTTP 请求。

服务端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容如下:


其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。

浏览器渲染过程


浏览器渲染过程大体分为如下三部分:

1)浏览器会解析三个东西:

一是 HTML/SVG/XHTML,HTML 字符串描述了一个页面的结构,浏览器会把 HTML 结构字符串解析转换 DOM 树形结构。


二是 CSS,解析 CSS 会产生 CSS 规则树,它和 DOM 结构比较像。


三是 Javascript 脚本,等到 Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。


2)解析完成后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。

  • Rendering Tree 渲染树并不等同于 DOM 树,渲染树只会包括需要显示的节点和这些节点的样式信息。
  • CSS 的 Rule Tree 主要是为了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每个 Element(也就是每个 Frame)。
  • 然后,计算每个 Frame 的位置,这又叫 layout 和 reflow 过程。

3)最后通过调用操作系统 Native GUI 的 API 绘制。

接下来我们针对这其中所经历的重要步骤详细阐述

构建 DOM

浏览器会遵守一套步骤将 HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:


浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。

将字符串转换成 Token,例如:<html>、<body>等。Token 中会标识出当前 Token 是“开始标签”或是“结束标签”亦或是“文本”等信息

这时候你一定会有疑问,节点与节点之间的关系如何维护?

事实上,这就是 Token 要标识“起始标签”和“结束标签”等标识的作用。例如“title”Token 的起始标签和结束标签之间的节点肯定是属于“head”的子节点。


上图给出了节点之间的关系,例如:“Hello”Token 位于“title”开始标签与“title”结束标签之间,表明“Hello”Token 是“title”Token 的子节点。同理“title”Token 是“head”Token 的子节点。

  • 生成节点对象并构建 DOM

事实上,构建 DOM 的过程中,不是等所有 Token 都转换完成后再去生成节点对象,而是一边生成 Token 一边消耗 Token 来生成节点对象。换句话说,每个 Token 被生成后,会立刻消耗这个 Token 创建出节点对象。注意:带有结束标签标识的 Token 不会创建节点对象。

接下来我们举个例子,假设有段 HTML 文本:

复制代码

<html>
<head>
 <title>Web page parsing</title>
</head>
<body>
 <div>
 <h1>Web page parsing</h1>
 <p>This is an example Web page.</p>
 </div>
</body>
</html>

上面这段 HTML 会解析成这样:


构建 CSSOM

DOM 会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建 CSSOM。

构建 CSSOM 的过程与构建 DOM 的过程非常相似,当浏览器接收到一段 CSS,浏览器首先要做的是识别出 Token,然后构建节点并生成 CSSOM。


在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

注意:CSS 匹配 HTML 元素是一个相当复杂和有性能问题的事情。所以,DOM 树要小,CSS 尽量用 id 和 class,千万不要过渡层叠下去

构建渲染树

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。


在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

我们或许有个疑惑:浏览器如果渲染过程中遇到 JS 文件怎么处理

渲染过程中,如果遇到<script>就停止渲染,执行 JS 代码。因为浏览器有 GUI 渲染线程与 JS 引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript 的加载、解析与执行会阻塞 DOM 的构建,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停构建 DOM,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建。

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。

JS 文件不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建

原本 DOM 和 CSSOM 的构建是互不影响,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也开始阻塞 DOM 的构建,只有 CSSOM 构建完毕后,DOM 再恢复 DOM 构建。

这是什么情况?

这是因为 JavaScript 不只是可以改 DOM,它还可以更改样式,也就是它可以更改 CSSOM。因为不完整的 CSSOM 是无法使用的,如果 JavaScript 想访问 CSSOM 并更改它,那么在执行 JavaScript 时,必须要能拿到完整的 CSSOM。所以就导致了一个现象,如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后在继续构建 DOM


布局与绘制

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

以上我们详细介绍了浏览器工作流程中的重要步骤,接下来我们讨论几个相关的问题:

几点补充说明

1.async 和 defer 的作用是什么?有什么区别?

接下来我们对比下 defer 和 async 属性的区别:


其中蓝色线代表 JavaScript 加载;红色线代表 JavaScript 执行;绿色线代表 HTML 解析。

1)情况 1<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

2)情况 2<script async src="script.js"></script> (异步下载)

async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。

3)情况 3 <script defer src="script.js"></script>(延迟执行)

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。

在加载多个 JS 脚本的时候,async 是无顺序的加载,而 defer 是有顺序的加载。

2. 为什么操作 DOM 慢?

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在 JS 的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”(如下图)。


过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。

3. 你真的了解回流和重绘吗?

渲染的流程基本上是这样(如下图黄色的四个步骤):

1. 计算 CSS 样式

2. 构建 Render Tree

3.Layout – 定位坐标和大小

4. 正式开画


注意:上图流程中有很多连接线,这表示了 Javascript 动态修改了 DOM 属性或是 CSS 属性会导致重新 Layout,但有些改变不会重新 Layout,就是上图中那些指到天上的箭头,比如修改后的 CSS rule 没有被匹配到元素。

这里重要要说两个概念,一个是 Reflow,另一个是 Repaint

重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。

回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来,这个过程就是回流(也叫重排)。

我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流 + 重绘或者只有重绘。

回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

1)常见引起回流属性和方法

任何会改变元素几何信息 (元素的位置和尺寸大小) 的操作,都会触发回流,

  • 添加或者删除可见的 DOM 元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度;
  • 内容变化,比如用户在 input 框中输入文字;
  • 浏览器窗口尺寸改变——resize 事件发生时;
  • 计算 offsetWidth 和 offsetHeight 属性;
  • 设置 style 属性的值。

2)常见引起重绘属性和方法


3)如何减少回流、重绘

  • 使用 transform 替代 top;
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);
  • 不要把节点的属性值放在一个循环里当成循环里的变量。

复制代码

for(let i = 0; i < 1000; i++) {
 // 获取 offsetTop 会导致回流,因为需要去获取正确的值
 console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局;
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame;
  • CSS 选择符从右往左匹配查找,避免节点层级过多;
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

性能优化策略

基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。

  • JS 优化: <script> 标签加上 defer 属性 和 async 属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。
  • defer 属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
  • async 属性: HTML5 新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
  • CSS 优化: <link> 标签的 rel 属性 中的属性值设置为 preload 能够让你在你的 HTML 页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能。

总结

综上所述,我们得出这样的结论:

  • 浏览器工作流程:构建 DOM -> 构建 CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  • CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕后才会进入下一个阶段构建渲染树。
  • 通常情况下 DOM 和 CSSOM 是并行构建的,但是当浏览器遇到一个不带 defer 或 async 属性的 script 标签时,DOM 构建将暂停,如果此时又恰巧浏览器尚未完成 CSSOM 的下载和构建,由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 构建完毕后再执行 JS,最后才重新 DOM 构建。

参考文章

  • https://segmentfault.com/q/1010000000640869
  • https://coolshell.cn/articles/9666.html
  • https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5c024ecbf265da616a476638
  • https://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ==&mid=2650588806&idx=1&sn=408a54e7c8102fd6944c9a40b119015a&chksm=8891d6a2bfe65fb42f493fe9a4dab672dd7e440f31e753196cee0cfbc6696e4f8dd3a669e040&mpshare=1&scene=1&srcid=1228ZrXsmbZKcgCSu7zTVDwy#
  • https://juejin.im/book/5b936540f265da0a9624b04b/section/5bac3a4df265da0aa81c043c
  • https://juejin.im/book/5c47343bf265da612b13e5c0/section/5c4737375188255de8397ae3
  • https://juejin.im/book/5a8f9ddcf265da4e9f6fb959/section/5a8f9f7bf265da4e82635e46

更多内容,请关注前端之巅。

、背景

Web 端实时预览 H.265 需求一直存在,但由于之前 Chrome 本身不支持 H.265 硬解,软解性能消耗大,仅能支持一路播放,该需求被搁置。
去年 9 月份,Chrome 发布 M106 版本,默认开启 H.265 硬解,使得实时预览支持 H.265 硬解具备可行性。

然而 WebRTC 本身支持的视频编码格式仅包括 VP8、VP9、H.264、AV1,并不包含 H.265。根据 w3c 发布的 2023 WebRTC Next Version Use Cases 来看,近期也没有打算支持 H.265 的迹象,因而决定自研实现 WebRTC 对 H.265 的支持。

2、DataChannel

背景说到 chrome 支持了 h265 的硬解,但 WebRTC 并不支持直接传输 h265 视频流。但可以通过 datachannel 来绕过这个限制

WebRTC 的数据通道 DataChannel 是专门用来传输除音视频数据之外的任何数据的(但并不意味着不可以传输音视频数据,本质上它就是一条 socket 通道),如短消息、实时文字聊天、文件传输、远程桌面、游戏控制、P2P加速等。

1)SCTP协议

DataChannel 使用的协议是 SCTP(Stream Control Transport Protocol) (是一种与TCP、UDP同级的传输协议),可以直接在 IP 协议之上运行。

但在 WebRTC 的情况下,SCTP 通过安全的 DTLS 隧道进行隧道传输,该隧道本身在 UDP 之上运行,同时支持流控、拥塞控制、按消息传输、传输模式可配置等特性。需注意单次发送消息大小不能超过 maxMessageSize(只读, 默认65535字节)。

2)可配置传输模式

DataChannel 可以配置在不同模式中,一种是使用重传机制的可靠传输模式(默认模式),可以确保数据成功传输到对等端;另一种是不可靠传输模式,该模式下可以通过设置 maxRetransmits 指定最大传输次数,或通过 maxPacketLife 设置传输间隔时间实现;

这两种配置项是互斥的,不可同时设置,当同为null 时使用可靠传输模式,有一个值不为 null 时开启不可靠传输模式。

3)支持数据类型

数据通道支持 string 类型或 ArrayBuffer 类型,即二进制流或字符串数据。

后续两种方案,都是基于 datachannel 来做

3、方案一 WebCodecs

官方文档: github.com/w3c/webcode…

思路: DataChannel 传输 H.265 裸流 + Webcodecs 解码 + Canvas 渲染。即 WebRTC 的音视频传输通道(PeerConnection) 不支持 H.265 编码格式,但可采用其数据通道(DataChannel)来传输 H.265数据,前端收到后使用 Wecodecs 解码、Canvas 渲染。

优点:

  • 直接传输 H.265 裸码流,无需额外封装,实现简单方便;无冗余数据,传输效率高
  • Wecodecs 解码延迟低,实时性很高

缺点:

  • 音频需额外单独传输、解码和播放,需处理音视频同步问题
  • 既有 sdk 基于 video 封装,webcodes 方案依赖 canvas,既有 video 相关操作,需要全部重写,比如截图,录像等操作
  • 由于线上各项目等历史原因,既有 sdk 改动大,时间上不允许

4、方案二 MSE

官方例子: github.com/bitmovin/ms…

思路:Fmp4封装 + DataChannel 传输 + MSE 解码播放。即先将 H.265 视频数据封装成 Fmp4 格式,再通过 WebRTC DataChannel 通道进行传输,前端收到后采用 MSE 解码, video 进行播放。

优点:

  • 复用 video 标签播放,无需单独实现渲染
  • 音视频已封装到 Fmp4 中,web 端无需考虑音视频同步问题
  • 整体工作量相比 Wecodecs 小,可快速上线

缺点:

  • 设备端实现 Fmp4 封装可能存在性能问题,因此需要云端转发实时进行解封装,或者前端解封装
  • MSE 解码实时性不好(云端首次切片会有 1~2 秒延迟)

相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】

音视频免费学习地址:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~

5、方案抉择

第一版本先以 MSE 上线。云端,前端开发量相对少,roi 高。

计划第二版上 wecodecs,不仅低延迟,而且可以避免云端耗流量的问题,节省成本。假设在第二版期间,WebRTC 官方支持了 H.265,那么直接兼容官方方案即可。


5.1 细说 Mse 及第一版 sdk 改造

Media Source Extensions, 媒体源扩展。官方文档: developer.mozilla.org/zh-CN/docs/…

引入 MSE 之后,支持 HTML5 的 Web 浏览器就变成了能够解析流协议的播放器了。

从另一个角度来说,通过引入 MSE,HTML5 标签不仅可以直接播放其默认支持的 mp4、m3u8、webm、ogg 等格式,还可以支持能够被 (具备MSE功能的)JS 处理的视频流格式。如此一来,我们就可以通过 (具备MSE功能的)JS,把一些原本不支持的视频流格式,转化为其支持的格式(如 H.264 的 mp4,H.265 的 fmp4)。

比如 B站开源的 flv.js 就是一个典型应用场景。B站的 HTML5 播放器,通过使用 MSE 技术,将 FLV源用 JS(flv.js) 实时转码成 HTML5 支持的视频流编码格式,提供给 HTML5 播放器播放。

// 此 demo 来自下面链接的官方示例, 可以直接跑起来,比较直观
// https://github.com/bitmovin/mse-demo/blob/main/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MSE Demo</title>
</head>
<body>
  <h1>MSE Demo</h1>
  <div>
    <video controls width="80%"></video>
  </div>

  <script type="text/javascript">
    (function() {
      var baseUrl = 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/dash/';
      var initUrl = baseUrl + 'init.mp4';
      var templateUrl = baseUrl + 'segment_$Number$.m4s';
      var sourceBuffer;
      var index = 0;
      var numberOfChunks = 52;
      var video = document.querySelector('video');

      if (!window.MediaSource) {
        console.error('No Media Source API available');
        return;
      }
        
      // 初始化 mse
      var ms = new MediaSource();
      video.src = window.URL.createObjectURL(ms);
      ms.addEventListener('sourceopen', onMediaSourceOpen);

      function onMediaSourceOpen() {
        // codecs,初始化 sourceBuffer
        sourceBuffer = ms.addSourceBuffer('video/mp4; codecs="avc1.4d401f"');
        sourceBuffer.addEventListener('updateend', nextSegment);

        GET(initUrl, appendToBuffer);
        
        // 播放
        video.play();
      }

      function nextSegment() {
        var url = templateUrl.replace('$Number$', index);
        GET(url, appendToBuffer);
        index++;
        if (index > numberOfChunks) {
          sourceBuffer.removeEventListener('updateend', nextSegment);
        }
      }

      function appendToBuffer(videoChunk) {
        if (videoChunk) {
          // 二进制流转换为 Uint8Array,sourceBuffer 进行消费
          sourceBuffer.appendBuffer(new Uint8Array(videoChunk));
        }
      }

      function GET(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.responseType = 'arraybuffer';

        xhr.onload = function(e) {
          if (xhr.status != 200) {
            console.warn('Unexpected status code ' + xhr.status + ' for ' + url);
            return false;
          }
          // 获取 mp4 二进制流
          callback(xhr.response);
        };

        xhr.send();
      }
    })();
  </script>
</body>
</html>

通过上面的 demo,以及测试(将 dmeo 中的 fmp4 片段换成我们自己的 IPC 设备(摄像头),H.265 类型的)得知,chrome 可以硬解 H.265 类型的 fmp4 片段。So,事情变得明朗了起来。大方向有了,无非就是 H.265 裸流,转换成 fmp4 片段,chrome 底层硬解。

5.2 fmp4 前端实时解封装

H.265 裸流解封装 fmp4,调研下来,如果纯 js 进行封装,工作量挺大。尝试用 wasm 调 c++ 的库,发现即使解封装性能也不大好。所以放在前端被 pass 掉了。

5.3 fmp4 云端实时解封装

性能好,对前端 0 侵入。确定了云端解封装,接下来讲讲这段时间开发遇到的核心链路演变,及最终的流程方案。

6、阶段一

云端实时解封装 Fmp4,写死 codecs(音视频编码类型) -> 前端 MSE 解码播放 -> 播放几秒后,失败,MSE 会抛异常,大概意思就是你的数据不对了,前后衔接不上。

排查下来,是 MSE 处于 updating 的时候,不能进行消费,数据直接被丢掉,导致后续数据衔接不上。那既然不能丢,我们就缓存下来。具体可以看下面的代码注释。

具体可以看代码注释:

const updating = this.sourceBuffer?.updating === true;
const bufferQueueEmpty = this.bufferQueue.length === 0;

  if (!updating) {
    if (bufferQueueEmpty) {
      // 缓存队列为空: 仅消费本次 buffer
      this.appendBuffer(curBuffer);
    } else {
      // 缓存队列不为空: 消费队列 + 本次 buffer
      this.bufferQueue.push(curBuffer);

      // 队列中多个 buffer 的合并
      const totalBufferByteLen = this.bufferQueue.reduce(
        (pre, cur) => pre + cur.byteLength,
        0
      );
      const combinedBuffer = new Uint8Array(totalBufferByteLen);
      let offset = 0;
      this.bufferQueue.forEach((array, index) => {
        offset += index > 0 ? this.bufferQueue[index - 1].length : 0;
        combinedBuffer.set(array, offset);
      });

      this.appendBuffer(combinedBuffer);
      this.bufferQueue = [];
    }
  } else {
    // mse 还在消费上一次 buffer(处于 updating 中), 缓存本次 buffer, 否则会有丢帧问题
    this.bufferQueue.push(curBuffer);
  }

考虑到 Fmp4 数据每一帧都不可丢失,因此 datachannel 走的是可靠传输。

但是测试下来,发现了新的问题。随着时间的增长,延迟会累积增大。因为丢包后,网络层会进行重试,重试的时间会累积进延时。我们测试下来,网络情况不好的时候,延迟会高达 30 秒及以上,理论上会一直增加,如果你拉流时间足够久的话

7、阶段二

ok,换个思路,既然不丢帧 + 可靠传输带来的延时问题完全不能接受,那么如果换用不可靠传输呢?

不可靠传输,意味着会丢帧。调研下来,Fmp4 可以丢掉一整个切片(一个切片包含多帧),既然如此,我们可以设计一套丢帧算法,只要判断到一个切片是不完整的,我们就把整个切片丢掉。

这样的话,理论上来讲,最多只会有一个切片的延迟,大概在2秒左右,业务层可以接受。

丢帧算法设计思路:在每一帧数据头部增加 4 个字节的数据,用来标识每一帧的具体信息。

  • segNum: 2个字节,大端模式,Fmp4片段序列号,从1开始,每次加1
  • fragCount: 1个字节,Fmp4片段分片总数,最小为1
  • fragSeq: 1个字节,Fmp4片段分片序列号,从1开始

前端拿到每帧数据后,对前 4 个字节进行解析,就能获取到每帧数据的详细信息。举个例子,假如我要判断当前帧是否为最后一帧,只需要判断 fragCount 是否等于 fragSeq 即可。

算法大致流程图:

具体解释一下:

  • frameQueue, 用来缓存每一帧的数据,用来跟后面一帧数据进行对比,判断是否为完整帧
  • bufferQueue, 此队列中的数据,都是完整的切片数据,保证 MSE 进行消费时,数据没有缺失
  /**
   * fmp4 切片队列 frameQueue,处理丢帧,生产 bufferQueue 内容
   *
   * @param frameObj 每一帧的相关数据
   *      每来一帧进行判断
   *      buffer中加上当前帧是否为连续帧(从第一帧开始的连续帧)
   *        是
   *          当前帧是否为最后一帧
   *            是 拼接buffer帧以及当前帧,组成完整帧,放入另外一个待消费 buffer
   *            否 当前帧入 buffer
   *        否 清空 buffer,当前帧入 buffer
   */

const frameQueueLen = this.frameQueue.length;
const frameQueueEmpty = frameQueueLen === 0;

  // 单一完整分片帧单独处理,直接进行消费
  if (frameObj.fragCount === 1) {
    if (!frameQueueEmpty) {
      this.frameQueue = [];
    }
    this.bufferQueue.push(frameObj.value);
    return;
  }

  if (frameQueueEmpty) {
    this.frameQueue.push(frameObj);
    return;
  }

  // 是否为首帧
  let isFirstFragSeq = this.frameQueue[0].fragSeq === 1;
  // 当前帧加上queue帧是否为连续帧
  let isContinuousFragSeq = true;
  for (let i = 0; i < frameQueueLen; i++) {
    const isLast = i === frameQueueLen - 1;

    const curFragSeq = this.frameQueue[i].fragSeq;
    const nextFragSeq = isLast
      ? frameObj.fragSeq
      : this.frameQueue[i + 1].fragSeq;

    const curSegNum = this.frameQueue[i].segNum;
    const nextSeqNum = isLast
      ? frameObj.segNum
      : this.frameQueue[i + 1].segNum;

    if (curFragSeq + 1 !== nextFragSeq || curSegNum !== nextSeqNum) {
      isContinuousFragSeq = false;
      break;
    }
  }

  if (isFirstFragSeq && isContinuousFragSeq) {
    // 是否为最后一帧
    const isLastFrame = frameObj.fragCount === frameObj.fragSeq;
    if (isLastFrame) {
      this.frameQueue.forEach((item) => {
        this.bufferQueue.push(item.value);
      });
      this.frameQueue = [];
      this.bufferQueue.push(frameObj.value);
    } else {
      this.frameQueue.push(frameObj);
    }
  } else {
    // 丢帧则清空 frameQueue,则代表直接丢弃整个 segment 切片
    this.emit(EVENTS_ERROR.frameDropError);
    this.frameQueue = [];
    this.frameQueue.push(frameObj);
  }

原本以为大功告成,结果意想不到的事情发生了。

当出现丢帧时,通过上面的算法,确实是把整个切片的数据丢弃掉了,但是 MSE 此时居然再次异常了,意思也是说数据序列不对,导致解析失败。

可是用 ffplay 在本地测试(丢掉一整个切片后,是可以继续播放的),陷入僵局,继续排查。

8、阶段三

话说最近 chatgpt 不是挺火,尝试着用了下,确实找到了原因。MSE 在消费 fmp4 数据时,需要根据内部序列号进行索引标识,因此即使是丢掉整个切片数据,还是会播放失败。怎么办?难道要回到不可靠传输?

经过一番权衡,最终决定,当出现丢帧时,前端通知云端,重新进行切片,并且此时前端重新初始化 MSE。

改造下来发现,效果还不错,我们把不可靠传输,datachannel 重传次数设置为 5。

出现丢帧的概率大大减小,就算出现丢帧,也只会有不到 2 秒的 loading,然后继续出画面,业务层可以接受。

最终,经过上面 3 个阶段的改造,就有了整个链路图。当然其实还有很多细节,没有讲到,比如利用 mp4box 获取 codec, 前端定时检查 datachannel 状态等,就不展开细说了。有兴趣的可以留言讨论

完整的链路图,简单画了下。

9、总结

目前 datachannel + MSE 的方案已经上线,测试下来,线上同时硬解 16 路没有性能问题。

后续会尝试用 webcodes 来进行 H.265 的解析,并处理音视频同步等问题。彻底解决掉延时的问题。

下一篇准备写日常排查 WebRTC 问题的一些思路,也欢迎评论区聊一下日常遇到的一些问题,下篇一起汇总。

原文 https://juejin.cn/post/7215608036394614844?searchId=20230814145744EF4FC3208E60C3F568D8