跳过导航
A 3D illustration of arrows pointing upwards around a 3D cloud, on a blue background.
走进 Sketch

技术探讨:加速我们 Web 应用中的检查器

我们深入探讨了最近 Web 检查器的性能改进,并解释了我们如何实现 2.3 倍的速度提升

我们最近推出了 对 Web 应用检查器进行了一些重大改进 — 此功能作为您的 Sketch 订阅的一部分提供,无需额外费用。最大的变化之一是显着的性能提升。

我们最大的重点始终是为您提供最佳体验。我们的用户每天都在突破 Sketch 的可能性,这不可避免地意味着更大、更复杂的文件。因此,我们努力改进检查器中的加载时间,以帮助每个人更有效地在浏览器中检查设计,并为新功能提供更多空间。现在,我们想深入探讨实现这一目标的技术故事。

专注于预处理

为了尽可能提高检查器的性能,我们预处理 Sketch 文件(通过 AWS lambda 函数),这为我们提供了所需的数据。 Sketch 文档可能很大,但因为您一次只检查一个画板,我们可以只加载您需要的数据,并保持交互性能。此预处理是我们想要加速的步骤。

当您想要提高性能时,首先要做的是衡量它。我们 Web 检查器的预处理器是用 Go 编写的,幸运的是 Go 开箱即用地配备了出色的分析工具。如果您的应用程序中已经有基准测试,Go 可以非常容易地分析 CPU 使用率

go test -bench BenchmarkParse -cpuprofile out.pprof

然后可以使用 go tool pprof out.pprof 分析生成的文件 (out.pprof)。

解析 JSON

这里可能会开个玩笑,有多少性能瓶颈问题最终都归结为某人在某个地方解析 JSON。因为我们的文件格式基本上是一个包含 JSON 文件的文件夹(以及一个开放规范——但更多内容将在未来的帖子中介绍),它也是我们今天讨论的核心。

我们发现大部分加载时间都花在了各种 JSON 解析函数内部(使用 jsoniter),而不是我们之后使用这些数据进行的处理。这并不令人惊讶;我们使用的测试文档包含 750MB 的未压缩 JSON 数据,将这些数据从纯文本解析回某种结构需要时间。

正如我们在上面提到的,因为 Web 应用程序允许您一次检查一个画板,所以我们不需要解析整个 Sketch 文档。考虑到这一点,我们最初的方法是将 JSON 解析成通用的 map[string]interface{} 映射。这样,我们可以在进一步处理之前简单地忽略我们未使用的画板的数据 — 这会将我们需要处理的 JSON 量减少到原始 JSON 加载的一小部分。然后我们使用 mapstructure 将剩下的数据转换为前端可以直接使用的有意义的 struct

检查我们的假设

当我们第一次进行此优化时,直觉上感觉这是一个正确的决定。但是用硬数据检查你的假设总是好的,对吗?为了确定解析为 map 或 structs 的速度有多快,我们编写了测试来衡量两者。

A graph comparing the time taken to process a file when parsing a map and parsing a script. Parsing a script is 2.9x faster at 2.23 seconds.

事实证明,直接将 JSON 解码为 structs(2.23 秒)比先将其转换为 map(6.51 秒)快近 3 倍。很明显,我们的首要任务是删除我们使用的 mapstructure 库。这样,我们可以将所有内容直接解析为 structs,然后删除之后我们不需要的页面和画板。一旦我们重写了我们 lambda 的很大一部分,我们就准备好进行初步比较了

A graph comparing the time taken to process a real document when parsing a map and parsing a script. Parsing a script is 1.6x faster at 3.35 seconds.

结果 — 从 5.27 秒 变为 3.35 秒 — 是一个非常好的速度提升!但我们正在努力,我们不能就此止步。也许我们可以利用 Go 著名的并发功能来进一步加速?

不幸的是……事实并非如此。在大多数情况下,Web 检查器 lambda 只处理 Sketch 文档中的单个 JSON 文件 — 毕竟,我们一次只查看一个页面或画板。不想承认失败,我们将注意力转向了主要的 document.json。此文件包含我们需要引用的共享文本和图层样式 — 也许那里有一个快速的胜利?几行代码之后,很明显,节省的… 略低于 8 毫秒。好吧,并非每个想法都能产生完美的结果!

解析点

由于并发功能无法帮助我们,我们再次回到 profiler 并注意到许多与 regex 相关的函数出现。我们知道我们在一个地方使用 regex 来解析点 — 我们将它们表示为 "{x, y}" 字符串,该字符串使用 regex 解析并转换为浮点数

re := regexp.MustCompile(`{([\\w\\.\\-]+),\\s?([\\w\\.\\-]+)}`)
parts := re.FindAllStringSubmatch(pointString, -1)
x, _ := strconv.ParseFloat(parts[0][1], 64)
y, _ := strconv.ParseFloat(parts[0][2], 64
return Point{X: x, Y: y}

查看我们的大型测试文档,我们注意到它包含超过一百万个点 — 许多图层,带有描述矢量路径的坐标和点。这感觉像是另一个潜在的瓶颈。 Regex 非常方便,但并非总是最快的,因此我们删除了 regex 并用一些手工制作的字符串解析代替了它

var point Point
pointString = strings.TrimLeft(pointString, "{")
pointString = strings.TrimRight(pointString, "}")
parts := strings.Split(pointString, ",")
x, _ := strconv.ParseFloat(parts[0], 64)
y, _ := strconv.ParseFloat(parts[1], 64)
point.X = x
point.Y = y
return point

它稍微快一些 — 但我们可以做得更好。这时灵感袭来:我们意识到这些点中的许多点都是相同的。为什么是这样?嗯,Sketch 文件格式描述了单位坐标中的所有矢量点(其中坐标系从 {0,0} 到 {1,1})。所以我们检查了一下,的确,在我们的测试文档中,几乎 70% 的点都是“{0, 0}”、“{0, 1}”、“{1, 0}”和“{1, 1}”。这真是个好消息 — 这意味着我们可以作弊!

var point Point
switch pointString {
case "{0, 0}":
	point.X = 0
	point.Y = 0
case "{1, 0}":
	point.X = 1
	point.Y = 0
// [...]
default:
    // parse string
}

但是它产生影响了吗?好吧,事实证明,“作弊”对性能非常好。我们将执行时间减少了 560 毫秒;从 3.34 秒到 2.78 秒。与我们开始时相比,处理速度现在几乎快了两倍。

A graph comparing the time taken to process a real document when parsing a map, parsing a script, and parsing a script and then parsing individual points. The combined improvements are 1.9x faster than the original method at 2.78 seconds.

部署更改

现在感觉是停止并测试性能改进在实际硬件上的外观的好时机,而不是在我们的开发机器上(在这种情况下为 M1 MacBook Air)。我们在服务器端使用大量 Mac 来处理 Sketch 文档,但有更大的部分在 AWS 上运行 Linux。

Two graphs comparing the time taken to process a document and the memory usage difference between the old method and the new method. The new method is 2.3x faster and uses 3.7x less memory.

最后,我们在我们的 AWS 测试服务器上运行了一个测试,并将结果与旧流程进行了比较。改进很明显,支持了我们之前的所有测试(我们在 M1 MacBook Air 上进行的)。

我们对结果感到非常满意 — 执行速度提高了 2.3 倍,内存使用率提高了 3.7 倍,甚至超过了我们的预期。经过一轮虚拟击掌,我们将其推广到我们所有的用户。您可以很清楚地看到我们切换到新代码的那一刻

A graph showing the real-world processing time of documents before and after the new method is introduced into production. The new method cuts the peak time down from around 19 seconds to around 7 seconds.

很少有文档像我们在所有这些测试中使用过的文档那样大而复杂,但在进行这样的改进时,使用极端情况通常很有用。查看我们的指标显示,p99 延迟(除 1% 异常值之外的所有文档处理的平均时间)从 6.25 秒 降至 1.97 秒 — 实现了 3.2 倍的稳固改进。

现在,我们处理绝大多数 Sketch 文档只需不到两秒钟的时间。考虑到其中几百毫秒用于从我们的存储服务器上拉取 Sketch 文件,我们对这个结果非常满意。我们希望您已经注意到日常工作中的改进,并且这些改进使您在 Sketch 中的工作更加顺畅。

您可能也喜欢

走进 Sketch

Sketch 和 AI

一段时间以来,我们一直在深入思考 AI 对 Sketch 的意义。以下是我们可能使用它的方式、我们永远不会使用它的方式以及指导我们思考的原则。

免费试用 Sketch

无论您是 Sketch 的新手,还是回来看看有什么新功能,我们都会在几分钟内完成设置,让您准备好做到最好。

免费开始
免费开始