如何在客户端上使用图像,同时保持流畅的UI? 界面开发人员Pavel Smirnov以在Market上搜索照片的经验为基础进行了讨论。 从报告中,您可以学习如何正确使用Web Workers和OffscreenCanvas。

-在这半小时内,我们将讨论冒险。 我将向您介绍我的冒险经历,并真的希望我的报告能激发您的灵感,并且您会在家中承担并做同样的事情。
首先,我想谈一谈我们的浏览器为我们提供的一些新技术或不是非常新的技术,它们可以使我们做一些很棒的事情。 但是在我看来,这并不是一件很有趣的事情,因为每个人都可以去MDN并阅读一些东西。 因此,我将讲述与市场团队一起完成的一项功能的故事。
让我们再次自我介绍。 我叫Pasha,我是Market团队的界面开发人员。

我主要处理移动界面-地图搜索,报价卡。 我还将代码从旧堆栈重写为新堆栈,然后从新堆栈重写为更高的堆栈。 我尝试使我的界面良好。 这里值得一说的是什么是好的界面。
好的接口具有不同的特性。 首先,它很方便;其次,它是美丽的;其次,它是负担得起的。 但是我今天要谈的特征之一是速度。 速度常常表现在他工作的流畅性上。 即使是细小的饰条也可以极大地改变我们界面的用户体验。

让我们继续今天的谈话计划。 首先,我们将讨论我所完成的任务:在市场上查找图片。 接下来,我将告诉您实现此功能必须解决的问题。 在这里,我们回顾一下您的脚本在浏览器中的工作方式,并了解对我有帮助的技术。 小破坏者:这些是Web Worker和OffscreenCanvas。
让我们回到任务。 几个月前,我们的产品经理Luba走近我。 Lyuba处理在市场上选择产品的问题。 现在我们有几种寻找商品的选择。 其中之一是在搜索栏中输入内容。

例如,“在萨马拉购买红色的iPhone X”。 我们会发现一些东西。 或者我们可以使用目录树。 在此目录中,我们有类别和子类别。
但是,如果我想在市场上找到某物,却不知道它叫什么,但我有这东西的图片,或者在某人的聚会上看到它,该怎么办?

我会讲一个真实的案例。 我曾经和朋友一起去咖啡馆。 我们在那罐子里点了柠檬水,这罐子有那么奇怪的东西。 我什至保留了一张照片。 这样做的目的是,当您将柠檬汁倒入玻璃杯中时,冰不会进入其中。 我们认为这是一件很酷的事情,但是对于这个东西的名称以及通常的用途,我们有不同的看法。 因此,我们在Yandex.Pictures上找到了它。
但是我想-如果我不仅可以搜索该商品,还可以立即购买它,或者至少找到价格,阅读评论,功能等,那就太酷了。在这一点上,我们的梦想与Any吻合,我们决定在市场上实现此类功能。
此功能是什么样的? 它允许用户上传照片或图片,甚至可以立即拍照并将其发送到市场。 我们使用Yandex搜索技术分析这张照片,在上面找到产品,并向用户展示这些产品的结果。 听起来似乎很简单,但如果那样简单,我就不做报告了。 为了确定这是什么功能,让我展示一下。
观看第一个演示我将在生产中展示。 首先,我上传我们想要的东西,然后看看会发生什么。
我们找到了一些商品,特别是这东西。 这东西叫做过滤器。 为了找到其他东西,我昨天在同事的办公桌上拍了一本书,让我们去找。 这是一本书,也许有人读过。 它称为“完美代码”。 他还以某种方式找到了它,并且出于某种原因,它的极限为18+。 这可能有点奇怪。
让我们回到我们的报告。 我遇到了什么问题? 第一个问题是用户开始下载任何东西,包括大图片。 例如,我的手机拍摄的照片大小为三到四兆字节,这很多。 将此类照片发送到后端效率很低。 这需要很长时间,分析它们也需要很长时间,因此您需要对此做一些事情。 但是,这里的一切都很简单-我们将在客户端上裁剪,压缩和调整此照片的大小。

我们将如何做? 我们有一个文件。 我们将以某种方式读取此文件。 我们将使用FileReader API进行阅读。 我会简单地告诉你它是什么。

这种浏览器API允许我们读取下载的文件并对其进行处理。 您可以用不同的方式阅读,我们现在来看。 这是它的功能,并且我们有某种对象通过change事件从输入中返回给我们。 让我们尝试阅读它。

该代码将如下所示。 这里没有什么复杂的。 我们有一个从FileReader构造函数创建的Reader对象,我们在该对象上挂载了load事件的开发人员。 接下来,我们将该文件读取为DataURL。 DataURL-一个字符串,代表通过Base64编码的文件的内容。 就像我们阅读时一样,我们需要以某种方式削减它。 首先,让我们将所有内容加载到图片中。 我们有一个tag或img元素,并将其加载到那里。

代码看起来像这样。 我们创建了一个img元素,通过load Reader事件,将行加载到src属性中,当行完成加载到img中时,我们将做进一步的工作。
我们将做我们想做的-裁剪图像。 我们将对其进行压缩,而Canvas这样的东西将为我们提供帮助,这是一个非常强大的工具。 它可以让您做很多事情。 但是在这里,我们只是在此画布上绘制图片,如果图片尺寸超出了最大允许范围,我们将使其稍微适合。 同样,我们可以使用具有所需压缩率的Canvas拾取这张图片。

这样的东西。 另一个小的免责声明:此处的代码已大大简化,我没有指定所有内容。 我们拥有错误处理等功能,但是为了使所有内容都能放到幻灯片上并且在报告中清晰可见,我省略了一些细节。
我们有图片大小,我们只看它们。 有一些常量允许我们使用。 如果图片的尺寸超过我们的常数,我们只需在它们下面修剪它们,然后将“画布”设置为这些相同的尺寸。
接下来,我们将在此画布上绘制图片。

以2d上下文为例,我们需要2d图像,然后尝试使用drawImage方法进行绘制。 DrawImage是一个有趣的方法,如果我没记错的话,它接受9个参数。 但是它们并不是全部强制性的,我们将仅使用五个。 我们取Image和这两个零,这是图片的偏移量或缩进量。 我们需要左上角。 用我们需要的尺寸绘制。
此外,从此Canvas中,我们将以完全相同的方式获取我们的DataURL编码的Base64字符串,并将其转换为blob-一个特殊的对象,对我们而言,它很容易发送到服务器。 似乎全部。 一切正常。 图片被裁剪,图片被发送,图片被识别。
但是后来我开始注意到一些事情。 当我测试此解决方案时,当我上传图片时(尤其是在较弱的设备上),我的界面变慢了一点。 要么没有按下按钮,要么元素没有滚动。 您是否感觉到您的代码在99%的情况下都能正常工作,但有时却无法正常工作? 您可以提供它进行测试,可能没人会注意到。 用户可能不会注意到,尤其是在功能较弱的设备上。
这从来没有发生过,我决定修复它。 原来这是一个问题。 如果图像很大,则在进行裁剪,压缩操作时,我们花费了一些时间,而在这很小的时间内,我们的界面没有响应。
起初,我弄清楚了为什么会这样。 这里需要记住一点JavaScript在浏览器中的工作方式。 我不会详细介绍,这是一个大型报告的主题。 只要记住一些要点即可。

我们有一个运行在单个线程中的JavaScript,我们称它为main。 在浏览器中,我们将其作为事件循环。 在这里,我们立即说这是一个模型。 在某些浏览器中,事件循环的组织方式不同,但顾名思义,通常是一个循环。 它按顺序处理队列中的某些任务。
一个不愉快的时刻:在他完成一项任务之前,他不会继续进行下一项任务。 我将展示我看到的演示,她将演示。 她是经典。
观看第二个演示我有一个以不同方式完成的GIF图像和CSS动画:一种使用translatex,另一种使用position:相对左侧,第三种使用JavaScript,即requestAnimationFrame。 这是刺猬在旋转的地方。 我该怎么办?
我将阻塞主线程五秒钟。 您知道,通常硬汉会计算第n个斐波那契数,但我写了一个无尽循环,有五秒钟的间隔。
会发生什么? 您立即注意到刺猬停止了旋转,并且使用了translatex动画的下部猫也停止了骑行。 但是,让我们在另一个浏览器(例如Safari)中查看相同的演示。 GIF猫停止运行。
我为什么要展示所有这些? 首先,浏览器不同,您必须考虑这一点。 其次,当我们的流程被某些事物阻塞时,某些事物将停止工作。 例如-JavaScript动画。 或者让我们证明文本将不再对我们突出,按钮将不再被按下。
这是一个非常抽象的例子。 我们不要阻塞流五秒钟,而要执行任务,上传照片,裁剪,挤压并在此处绘制。 我们不会将其发送到任何地方,也不会很明显。
观看第三个演示我在这里有一台功能强大的MacBook,为了使所有内容看起来更具说服力,我们将处理器速度降低了六倍。 这使您可以执行DevTools。 上载我们的照片。 完美密码将再次为我们提供帮助。 如我们所见,与阻塞主线程时发生的事情相同。
然后让我们回到我们的任务,并思考我们将如何处理这一问题。

顺便说一句,如果您查看探查器,我们将看到此情况。 在红色框中是我们的微任务,它阻塞了主线程。 我们看到他将其阻挡了近五秒钟。 它在一台功能强大的计算机上,而在功能较弱的设备上,它将更加引人注目。
让我们继续解决方案。 我将立即说出我使用过的东西和所做的事情,然后我们将分析所有这些事情。 首先,我使用了Web Workers。 它们使我们可以将一些任务放到单独的线程中。 其次,在Web Workers的上下文中,DOM对我们不可用。 为了处理这种情况,我们将使用其他工具。 我们将无法使用图片,可以使用经典的Canvas,因此我们使用Canvas和其他技巧。

让我们快速记住工人是什么,他们是干什么的。 它们允许您在一个单独的线程中运行JavaScript,而不是主要运行。 并且Workers流不会干扰主界面的呈现流程。 因此,我们可以执行一些复杂的计算任务,而不会降低接口速度。
我们有一个工具,可让您将某些东西转移到Workers并从Workers退还一些东西。 让我们来看一个例子。

因此,我们使用构造函数创建了Worker。 在那里,您需要将路径传输到文件。 我们甚至可以通过blob。 我们有一个Message事件处理程序。 在这种情况下,它将仅在屏幕上显示一些内容。 然后,我们可以向我们的工作人员发送一些数据。

有什么支持? 这里一切都很好。 工人是一个众所周知的工具,而不是一个新工具,但是我的许多朋友认为它们并不总是得到支持。 事实并非如此。

现在让我们看一下OffscreenCanvas。 正如我们已经看到的,Canvas是一个非常强大的工具,但是不幸的是,在Web Workers的上下文中它对我们不可用,因此我们将使用替代方法。 这是一个相当新的东西,称为OffscreenCanvas。 它允许您执行与Canvas相同的操作,只是在屏幕外,即在Web Workers上下文中。 当然,我们也可以在window上下文中执行此操作,但是现在我们不能。

有什么支持? 如您所见,有很多红色。 通常,Chrome仅支持OffscreenCanvas。 Firefox也有一个选项,但是到目前为止有一个标志,并且Canvas仅适用于WebGL上下文。 在这里,您可以问-为什么我要谈论像OffscreenCanvas这样的很酷的东西,它在任何地方都无法使用?

一个小题外话。 我们在Market中提供了一定程度的浏览器支持。 我们有两个数量。 一个值代表浏览器的特征,我们根本不支持它。 这大约是浏览器普及率的一半。
还有第二个数量。 它包括我们支持的浏览器,但仅包含关键功能。 在这里,没有Workers,所有搜索功能都可以工作,但是带有细小的装饰。 我认为可以,我们的团队认为可以。 让我们看看我们将如何实现这一点。

这是我们将要做的事情的图表。 我们甚至拥有可以通过FileReader读取的文件。 但是在主流中,我们将其发送给Web Workers,在Web Workers中将其进行剪切,压缩并返回给我们,并且我们已经将其发送至服务器。

让我们看看我们的工作者的代码。 首先,我们创建一个具有所需宽度和高度的OffscreenCanvas实例。
此外,正如我所说,Image元素在Workers上下文中不可用,因此在这里我们使用createImageBitmap方法,这将使我们能够表征图片的数据结构。
从有趣的角度来看:我们在这里看到了自我。 那些不熟悉Web Workers的人指的是执行上下文。 对我们来说,无论是在窗口还是在此,我们都使用自我。 此方法是异步的,为了紧凑和方便,我在这里使用了wait,为什么不呢?
接下来,我们将获得与之前相同的图像并执行相同的操作。 在画布上绘制并返回。
从简单开始。 我们曾经采用DataURL并将所有内容转换为blob。 但是在这里,convertToBlob方法可以立即使用。 为什么以前没有使用过? 因为支持差。 但是,由于我们一直在这里使用OffscreenCanvas,是什么使我们无法使用convertToBlob?

我们将基本上从此blob返回一个流,然后将其从该流发送到服务器。 或者,如演示中所示,绘制它。
因此,我们在主线程中创建了一个Worker,侦听来自它的一些消息,然后将其绘制或发送到服务器。 这里没有什么重要的。 工人将接受我们的文件。
让我们回到我们的演示。
观看第四个演示所有相同的演示,所有相同的三只猫和一只刺猬。 我将再次打开节流阀,使处理器速度降低六倍。 我将上传同一张照片。 如我们所见,在绘制图片时,动画没有停止,刺猬继续旋转,界面仍然存在,我们达到了想要的效果。
但是这个决定可以改善吗?

顺便说一下,这里是探查器。 在这里,我们看不到之前看到的五秒钟内巨大的微任务。
改善是可能的。 使用可转移对象。 在这里值得再次返回。 当我们通过postMessage机制传递DataURL或blob时,我们复制了此数据。 这可能不是很有效。 避免它会很酷。 因此,我们提供了一种机制,使您可以像在软件包中一样将数据传输到Web Workers。
为什么我说“喜欢”? 当我们将这些数据传输给工人时,我们会在主流中失去对它们的控制-我们无法以任何方式与它们进行交互。 这里有第二个限制。 我们无法将所有数据类型传输到Web Workers。 我们不能使用字符串来执行此操作;我们将以不同的方式进行操作。

让我们看一下代码。 首先,我们传输数据有些不同。 这是我们的postMessage。 您会看到,有一个带有loadEvent.target.result的数组。 这样的接口使我们可以将数据作为可传输对象传输,从而失去对它们的控制。
顺便说一下,任何用Rust编写的人都可能会听到一些熟悉的东西。 而且我们将读取文件的不是字符串形式,而是ArrayBuffer形式。 这是激光雷达二进制数据流,无法直接访问。 因此,我们将不得不与他们做其他事情。

回到我们的ImageWorkers。 在这里,它变得更加有趣。 首先,我们使用缓冲区并执行Uint8ClampedArray这样的糟糕事情。 这是一个类型化数组。 顾名思义,其中的数据是符号号,即从零到255的数字将代表我们的图像像素。
第三个参数,我们传递了一个奇怪的东西,即宽度乘以高度乘以4。 为什么是四个? 确实是RGBA。 每种颜色有3个值,每个alpha通道有1个值。
接下来,我们将从该数组中生成ImageData,这是一种可以轻松在画布上绘制的特殊数据类型。 这里没什么有趣的。 我们只是获取一个数组并将其传递给构造函数。 此外,我们以相同的方式在画布上绘制图片,但是在ImageData下使用了不同的方法。 此外,一切都与以前相同。
让我们继续得出结论。 今天,我向您介绍了我不久前没有完成的一项任务。 我在其中注意到了什么?

界面的平滑度非常重要。 当用户稍稍滞后,有点冻结时,未按下该按钮,则可能导致UX严重恶化。 浏览器的工作方式不同。 我们看了一个有关Safari和Yandex.Browser的球形示例。 我们看到,如果您在一个浏览器中检查了界面的平滑度,则应该查看其他浏览器。
如果阻止脚本持续很长时间,则需要对阻止脚本执行某些操作。 就我而言,我将其放在Web Workers上。 但是可能还有其他方法,您可以以某种方式将它们分成较小的方法,在这里您必须考虑一下。 , Web Workers, .
? . . . , 200 , .
Web Workers . , , .
:
.