设计与算法 | Google Photos Web UI

本文介绍了Google Photos Web UI的设计与实现,重点在于如何处理全浏览器宽度自适应布局、随机浏览、60fps帧率以及瞬时加载等挑战。通过分段加载、自适应布局算法和优化渲染性能,实现了高效且流畅的照片浏览体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

640?wx_fmt=jpeg

作者 / Antin Harasymiv, UX Engineer, Google
* 很多时候,体验设计和算法的联系会比想象中要紧密得多。本文将从代码和体验两个层面和大家深度分享。

几年前,我有幸成为了 Google Photos 团队的工程师,并参加了 2015 年的首发活动。很多人为这个产品做出了贡献,从设计师、产品经理、研究人员,到无数的工程师 (横跨了 Android、iOS、Web 和服务器端),而这还只是参与到这个产品里的主要角色的一部分而已。我负责的是 Web UI,更具体地说,是照片的网格布局。

我们的目标——支持全浏览器宽度自适应布局,保留每张照片原本的宽高比,可随机浏览 (允许用户随时跳转到照片库中的任意位置),能处理数以十万计的照片,以及确保 60fps 的帧率,且做到照片的瞬时加载

在当时没有其他照片库能够做到上面提到的所有特性。据我所知,现在除我们之外仍然没有。虽然许多照片库现在支持其中的一部分功能,但它们通常会对每张照片进行正方形裁剪,这样整个布局才能正常运作。

我们是怎么做的呢?以下是关于我们如何解决这些挑战的技术分享。

难点在哪里?

其中两个最大的挑战来自数据量。

第一个挑战是,对于拥有大量照片的用户 (一些用户上传的照片超过 25 万张),元数据太多了。即使仅发送最精简的信息 (照片 URL、宽度、高度和时间戳),整个照片集都将产生出大量的数据 (MB 级别),而这直接和我们 "瞬时加载" 的目标冲突。

第二个挑战是照片本身。在使用现代 HDPI (高像素密度) 屏幕的情况下,即使是照片缩略图,其体积通常也会达到 50KB 或更多。1,000 个缩略图的文件体积可达 50MB,下载起来相当费劲,而且如果您想要立刻将它们全部放在网页中,就会拖慢浏览器的速度。旧版的 Google+ Photos 滚动浏览 1,000-2,000 张照片后就会变得迟缓,Chrome 的标签页最终会在加载 10,000 张照片后崩溃。

下面我们开始逐项详细讨论,具体分下面几个课题: 
  • 随机浏览:  能够快速跳转到照片库的任何位置。
  • 自适应宽度布局:  用照片填充浏览器的宽度,并保留每张照片的宽高比 (不采用正方形裁切)。
  • 60fps 帧率:  确保页面即使在查看数千张照片时仍能保持快速流畅的响应。
  • 瞬时加载:   最大限度地减少加载时间。

1. 随机浏览

大型照片库的页面布局有几种常用的方法。最古老的方法是分页显示,也就是每页显示固定数量的照片,然后用户需要单击 "下一页" 才能查看后续,不得不说这种体验让人厌烦。现在比较流行的一种方法是无限滚动,叫这个名字是因为,虽然您最初只能加载出一定数量的图片,但当您滚动到接近页面底部时,系统会自动加载出下一批图片,并将其插入当前的页面,如此反复。如果这个功能做得足够好的话,用户可以不断地向下滚动页面 (这也是 "无限滚动" 这个名字的来由)。

分页和无限滚动都有一个类似的缺点: 用户需要按照先后顺序加载完所有内容才能到达终点,所以如果您只是想快速找到一张几年前拍的照片,可能会非常麻烦。

我们把目光挪开一些,看看普通的文档编辑软件,其滚动条的运作方式大体上令人满意,您还可以直接拖拽滚动条,让文档快速跳过一些章节,直接抵达末尾,或是任何您想要停留的地方。不过在处理照片库的时候,如果使用分页显示,滚动条将会抵达当前页面的底部 (注意只是页面底部,而不是照片库末尾),而使用无限滚动的页面,其滚动条始终在变化,如果将滚动条拖动到页面底端,您会注意到随着页面的变长,它会稍微缩小。


随机浏览的网格系统提供了第三种可能——让滚动条能够符合直觉地正常运作。

为了支持随机跳转到照片库的任何部分,我们需要在页面上预先分配容器空间,以便使滚动条的尺寸和整体页面高度保持固定的比例。如果我们能够获得所有照片的信息,那这个任务就会变得相对容易,但由于照片数量可能太大而导致传输比较费时,我们需要另想办法才行。

这个时候,很多提供照片库服务的公司会选择走捷径——将每张照片裁切成相同的方形。这样,您只需要知道照片总数,就可以计算出整个页面的布局: 在方形的尺寸已经给定的前提下,您可以轻松根据页面宽度来计算列数和行数: 


const columns = Math.floor(viewportWidth / (thumbnailSize + thumbnailMargin));	
const rows = Math.ceil(photoCount / columns);	
const height = rows * (thumbnailSize + thumbnailMargin);

只需三行代码就可以完成布局,再使用十几行代码就可以完成对任意照片的渲染和定位。

我们减少元数据初始加载体积的方法是,将用户的照片集进行一层额外的分割,并在初始加载时只发送分割出来的单元以及该单元中所包含照片的数量。例如,一个简单的对照片集进行分割的方法是,按月份分割——您可以直接在服务器上进行分割计算 (或者预先算好),这样即使是时间跨度长达数十年的数百万张照片,其产生出的数据量仍然不会很大。一个比较典型的数据样本如下: 

{	
  "2014_06": 514,	
  "2014_05": 203,	
  "2014_04": 1678,	
  "2014_03": 973,	
  "2014_02": 26,	
  // etc...	
  "1999_11": 212	
}

△ 按月分割,每个月包含一个数值,即该月中照片的数量。

在极端情况下,对于在特定月份拍摄大量照片的用户 (如专业摄影师) 来说,这种做法仍然会有问题——分割处理的目标是将每个区块的元数据量减少到可控的程度,但对于重度用户来说,一个月可能会拍出数千张照片 (这意味着很多 MB 的数据)。不过我们的基础架构团队构建出了一个精巧的解决方案,在分割时整合了各种因素,例如地理位置信息、相近的时间戳等等,从而为每个用户创建出定制的分割规则。

通过分割信息,客户端可以估算每个单元需要占用多少尺寸,并将占位符放入网页 DOM 中,当用户快速滚动时,客户端就会从服务器中检索相应的照片元数据,计算完整布局并更新页面。

在客户端上,一旦我们获得了某个分段的元数据,我们就会更进一步,将每个分段 (section) 中的照片按照日期划分出子分段 (segment)。我们还讨论了动态子分段 (例如按地理位置、人物、日期等要素) 技术,将来有可能会由此发展出很棒的功能。

640?wx_fmt=png
△ 一个照片集被分割成分段、子分段,以及一个个单元格。
估算一个分段的尺寸是非常简单的,您可以只取一个分段的照片数量,然后将最常用的照片宽高比带进去做乘法即可: 

// Ideally we would use the average aspect ratio for the photoset, however assume	
// a normal landscape aspect ratio of 3:2, then discount for the likelihood we	
// will be scaling down and coalescing.	
const unwrappedWidth = (3 / 2) * photoCount * targetHeight * (7 / 10);	
const rows = Math.ceil(unwrappedWidth / viewportWidth);	
const height = rows * targetHeight;

您可能会问:  这样的乘法一点都不精确吧?的确如此,而且还可能差得很远。

幸运的是,我最初把这部分的问题想得过于复杂了 (我将在下文中的布局部分对此加以解释)。事实证明,您不需要估得非常准 (如果照片数量很大,偏差达数十万像素之多都有可能)。唯一重要的,您的估算需要具有大致上的代表性,让滚动条可以大致准确地表达出比例感。

分享一个简单的诀窍: 当您最终加载出了一个分段的照片时,您可以计算实际高度与估测高度之间的差异。如果存在差异,只需将其下方的所有分段根据该差值垂直移动即可。

另外,如果您正在加载滚动点上方的部分,那么您可能还需要更新一下滚动位置 (scroll position)。不用担心,所有这些计算和位移都可以在眨眼间完成,耗时差不多一个动画帧,因此用户不会感到任何异常。

640?wx_fmt=gif

△ 完成实际的加载后,根据高度差值更新布局。

2. 自适应宽度布局

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值