Как Google Фото обеспечивает естественное взаимодействие с пользователем

внешний интерфейс алгоритм Google
Как Google Фото обеспечивает естественное взаимодействие с пользователем

Авторизован для перевода, исходный адрес: https://medium.com/google-design/google-photos-45b714dfbed1

Я имел честь присоединиться к команде Google Фото в качестве инженера несколько лет назад и работать над первым выпуском в 2015 году. Задействовано бесчисленное количество дизайнеров, менеджеров по продуктам, ученых и инженеров (включая платформы, интерфейсные и серверные части), и вот лишь некоторые из основных обязанностей. Я отвечал за часть веб-интерфейса, точнее, за сетку фотографий.

Наша цель — сделать идеальный макет: поддержкаПолноэкранный адаптивный, чтобы обеспечить исходный масштаб изображения, взаимодействиеудобный(Например, пользователь может перейти в указанное место), что не только отображает большое количество изображений, но и гарантирует, что страницавысокая производительностьа такжевысокоскоростная загрузка.

В то время на рынке не было фотоальбомов, которые могли бы достичь всех вышеперечисленных эффектов. Насколько я знаю, пока нет продукта, сравнимого с Google Фото. Особенно с точки зрения макета страницы и соотношения изображений, большинство продуктов по-прежнему обрезают изображения в квадраты, чтобы обеспечить красивый макет.

Ниже я расскажу, как мы справились с этими задачами, а также некоторые технические подробности в веб-версии Google Фото.

Почему эта задача такая сложная?

Есть два больших препятствия, связанных с «размером».

Первая проблема «размера» связана с огромным объемом изображений (некоторые пользователи загрузили более 250 000 изображений) с большим количеством метаданных, хранящихся на сервере. Даже если количество информации, которое должно быть передано одним изображением (например, URL-адрес изображения, ширина и высота, отметка времени...) невелико, из-за большого количества изображений это напрямую приводит квремя загрузкиудлинить.

Вторая проблема «размера» — это само изображение. На современных экранах высокой четкости маленькое фото не менее 50Кб, а 1000 таких фото - 50Мб. Мало того, что сервер будет медленно передавать данные, но, что еще хуже, рендеринг такого большого количества контента одновременно приведет к сбою браузера. Ранние версии Google+ Фото зависали при загрузке 1000–2000 изображений, а вкладка браузера зависала при загрузке 10 000 изображений.

Ниже я расскажу, как мы решили эти две проблемы, в четырех частях:

  1. «Независимые» картины— Быстро найти указанное место в библиотеке фотографий.

  2. Адаптивная верстка— Максимально заполните изображение в соответствии с шириной браузера и сохраните исходные пропорции изображения (без обрезки квадратов).

  3. Плавная прокрутка со скоростью 60 кадров в секунду— В условиях огромного объема данных также необходимо обеспечить плавное взаимодействие страниц.

  4. Своевременная обратная связь- Минимальное время загрузки.

1. «Самостоятельные» картинки

Думаю, вы видели много схем отображения данных. как самый традиционныйнумерация страниц, на каждой странице отображается фиксированное количество результатов, нажмите «Далее», чтобы получить новые данные, и вы можете просмотреть все результаты вперед и назад; теперь более популярным методом являетсябесконечная прокрутка, загружать количественные данные за один раз и автоматически извлекать новые данные и вставлять их на страницу, когда пользователь прокручивает страницу ближе к концу текущих данных. Если весь процесс проходит достаточно гладко, вы можете прокручивать всю страницу вниз — так называемая бесконечная прокрутка.

Но и у пагинации, и у бесконечной прокрутки есть проблема: после загрузки всех данных, если пользователь хочет найти первую фотографию — кошмар.

Для большинства страниц пользователь также может перемещаться по полосам прокрутки. Но для пагинации полоса прокрутки может располагаться максимум внизу текущей страницы, а не последней картинки всей библиотеки изображений, для бесконечной прокрутки положение полосы прокрутки всегда будет меняться, если только все данные не передаются на client , в противном случае не пытайтесь использовать полосу прокрутки.

Независимая сетка изображений дает еще одну идею, в которой полоса прокрутки будет вести себя нормально.

Чтобы пользователь использовать полосу прокрутки, чтобы найти указанное положение, нам нужно зарезервировать пространство страницы. Если все фотографии пользователя могут быть отправлены одновременно, это нормально для достижения; но проблема в том, что объем данных слишком велик, чтобы быть сделаны одновременно. Похоже, нам нужно попробовать что-то еще.

Это также проблема, с которой приходится сталкиваться другим библиотекам изображений.Чтобы выполнить макет заранее, обычное решение состоит в том, чтобы квадратировать все изображения. Для этого метода нужно знать только общее количество изображений: разделите ширину области просмотра на определенную площадь, чтобы получить количество столбцов, а затем передайте общее количество изображений, чтобы получить количество строк.

  1. const columns = Math.floor(viewportWidth / (thumbnailSize + thumbnailMargin));

  2. const rows = Math.ceil(photoCount / columns);

  3. const height = rows * (thumbnailSize + thumbnailMargin);

скопировать код

Это можно сделать с помощью трех строк кода, а общую компоновку можно выполнить менее чем за двенадцать строк кода.

Чтобы уменьшить первую передачу метаданных, мы думаем разделить фотографии пользователя на отдельные модули и передавать только имя модуля и количество фотографий под каждым модулем при первой загрузке. Например, разделить модули по «месяцу» — этот шаг может быть реализован на стороне сервера (то есть рассчитан заранее). Если количество данных достигает миллионов, это может исчисляться даже «десятью годами». Данные, используемые при загрузке в первый раз, выглядят так:

  1. {

  2.  "2014_06": 514,

  3.  "2014_05": 203,

  4.  "2014_04": 1678,

  5.  "2014_03": 973,

  6.  "2014_02": 26,

  7.  // ...

  8.  "1999_11": 212

  9. }

скопировать код

Если пользователь (например, фотограф) может сделать большое количество снимков за один и тот же период времени, эта схема все равно порочна — причина разделения данных на модули — облегчить обработку метаданных, но для активных пользователей объем данных каждый месяц по-прежнему огромен. Отличная команда Foundation Services предложила решение, позволяющее пользователям создавать собственные таксономии (такие как местоположения, метки времени и т. д.).

С помощью этой информации мы можем разместить место для каждого модуля. Когда пользователь быстро прокручивает страницу, клиент получает соответствующие метаданные изображения, вычисляет полный макет и обновляет страницу.

На стороне браузера, после получения метаданных модуля, мы снова организуем фотографии по дате. Динамические группировки, которые мы обсуждали (например, по местоположению, человеку, дате...), также были бы отличными функциями.

Теперь очень просто прикинуть размер модуля.После подсчёта количества фотографий и предполагаемого соотношения одной фотографии:

  1. // 理想情况下,我们应该先计算出当前模块的比例均值

  2. // 不过我们先假设照片比例是 3:2,

  3. // 然后在它的基础上做一些调整

  4. const unwrappedWidth = (3 / 2) * photoCount * targetHeight * (7 / 10);

  5. const rows = Math.ceil(unwrappedWidth / viewportWidth);

  6. const height = rows * targetHeight;

скопировать код

Как вы могли догадаться, такие оценки неточны и даже сильно отклоняются.

сначала все усложняю(макетПо ссылке будет подробно рассказано), но по результатам необязательно получать точные значения с самого начала (в случае большого количества фотографий оно может отклоняться даже на тысячи пикселей). Причина, по которой мы сделали оценку, заключалась в том, чтобы гарантировать положение полосы прокрутки, и оказалось, что даже это грубое позиционирование полосы прокрутки все еще работает.

Вот небольшая хитрость.Когда модуль действительно загружен, браузер будет знать разницу между фактической требуемой высотой посадочного места и предполагаемой высотой посадочного места.Просто переместите оставшиеся модули страницы вниз на разницу высот.Дистанция подойдет.

Если загружаемый модуль находится над окном просмотра, то положение полосы прокрутки необходимо обновить после загрузки модуля. Все операции обновления могут быть выполнены с одним анимационным кадром в течение одной секунды, и воздействие на пользователя невелико, если скорость достаточно высокая, пользователь может быть даже незаметен.

2. Адаптивная верстка

Насколько я знаю, основная адаптивная компоновка изображений на рынке использует умный и простой метод: каждая строка имеет разную высоту, но заполняет область просмотра, а изображения в одной строке масштабируются в соответствии с соотношением сторон, чтобы гарантировать, что та же строка находится в той же строке высота изображения. Пользователь также не заметит разницу в высоте от строки к строке.

Откажитесь от одинаковой высоты всех изображений, обеспечьте пропорции исходного изображения, а затем исправьте расстояние между изображениями. Реализуется несложно, найти самый верхний ряд, масштабировать каждое фото по соотношению сторон, обновить текущую ширину сетки, если обнаружится, что она превышает ширину вьюпорта, уменьшить каждое изображение в ряду пропорционально, конечно, это time Высота строки также будет меньше.

Например, когда есть 14 картинок:

Этот метод очень экономичен, Google+ использовал этот метод в прошлом, а поиск Google использует усовершенствование этого метода, но это все та же концепция. После оптимизации Flickr (дальше они сравнили, лучше ли поместить на одно изображение меньше или на одно больше, когда оно вот-вот превысит ширину области просмотра) и открыли исходный код своего решения. Упрощенная версия выглядит следующим образом:

  1. let row = [];

  2. let currentWidth = 0;

  3. photos.forEach(photo => {

  4.  row.push(photo);

  5.  currentWidth += Math.round((maxHeight / photo.height) * photo.width);

  6.  if (currentWidth >= viewportWidth) {

  7.    rows.push(row);

  8.    row = [];

  9.    currentWidth = 0;

  10.  }

  11. });

  12. row.length && rows.push(row);

скопировать код

Сначала меня (на самом деле зря) волновало, что оценки далеки от окончательных значений, и проблема становилась все сложнее. Но тем временем я случайно нашел решение.

Моя философия такова: проблемы макета сетки изображения и переноса текста похожи. Ссылаясь на полностью задокументированный алгоритм разбиения строк Кнута и Пласса, я планирую применить его к макету изображения.

В отличие от обтекания текстом, проблему в модулях нужно рассматривать с точки зрения макета изображения: каждая строка в модуле будет влиять на макет строк после них.

Базовыми единицами алгоритма K&P являются коробка, клей и пенальти. Коробка — это каждый неотделимый блок, а также объект, который мы хотим найти.В макете статьи коробка — это слово или отдельный символ, клей — это пространство между коробками, которое является пространством для текста, и их можно тянуть Расширять или сжимать; чтобы предотвратить двойное разделение Box, вводится понятие Penalty, а общим Penalty является дефис или новая строка.

Посмотрите на картинку ниже, вы заметили, что ширина клея между коробками не определена:

Проблема переноса строки изображения проще, чем усечение текста. Для текста можно использовать различные схемы усечения — добавление пробелов между словами, увеличение межсловного интервала и использование дефисов. Однако в картинной сцене, если ширина промежутка между картинками разная, пользователь это обязательно обнаружит, понятия «дефис картинки» нет.

Вы можете прочитать больше об алгоритме переноса текста здесь, который не будет расширяться в этой статье. Возвращаясь к теме изображений, мы будем использовать только что упомянутый алгоритм для реализации нашей упаковки изображений.

Чтобы применить его к макету изображения, мы хотим отказаться от концепции Glue напрямую, а затем упростить использование Penalty и рассматривать изображение как Box. Сказав это, вероятно, более уместно, что мы отказались от Коробки и сохранили Клей, предполагая, что именно изображения, а не их интервалы, имеют переменный размер. Или просто подумайте, что размер нашей коробки не меняется.

Не изменяя интервал между изображениями, мы решили отрегулировать высоту строки, чтобы настроить макет. В большинстве случаев для разрывов строк требуется дополнительное место. При предварительном переносе строк, чтобы обеспечить заполнение ширины, вертикальное пространство будет увеличено, потому что исходная строка должна быть выше; наоборот, при отложенном переносе высота строки будет укорочена. Найдите наиболее подходящее решение по размерам, рассчитав все возможности.

Теперь нам нужно рассмотреть только три вещи: идеальный рядвысоко,максимумкомпрессиякоэффициент (насколько короче может быть сжата высота строки) и макс.протяжениекоэффициент (или насколько высоко он может растянуться).

Принцип алгоритма: проверять по одной фотографии за раз, ища возможные разрывы строк — например, при увеличении группы фотографий их высота должна быть в заданном диапазоне (maxShrink ≤ высота изображения ≤ maxStretch). Всякий раз, когда вы найдете позицию, которую можно использовать в качестве точки разрыва строки, запишите ее, а затем продолжайте искать ее на основе этой позиции, пока не проверите все картинки и все возможности разрыва строки.

Например, на 14 картинках ниже в одну строку можно поставить три или четыре картинки. Если в первой строке три изображения, точка разрыва строки второй строки может быть на шестом или седьмом изображении; если в первой строке четыре изображения, точка разрыва строки второй строки будет на седьмом или седьмая картинка Восьмая позиция. Посмотрите, точка разрыва строки предыдущей строки будет определять расположение следующей картинки, но независимо от того, где она обрезана, это всегда будет расположение сетки.

Последним шагом является вычисление «значения плохости» каждой строки, которое показывает, насколько плоха текущая схема переноса. Для строки с той же высотой, что и наша предустановка, плохое значение равно 0; чем больше высота строки сжата/растянута, тем больше значение, другими словами, тем менее идеален макет строки. Наконец, выполняются некоторые вычисления для преобразования оценок каждой строки в значение (называемое недостатком). Во многих статьях написаны связанные формулы, обычно суммирующие неверные значения, затем берущие квадрат или куб и добавляющие некоторые константы. В Google Фото мы используем сумму в степени отношения максимального значения растяжения (чем менее идеальна высота строки, тем больше будут недостатки).

Конечным результатом является «график», каждый узел которого представляет собой изображение, это изображение является точкой разрыва линии, а каждое ребро представляет собой линию (узел может быть соединен с несколькими ребрами, что означает, что будет больше от задняя часть изображения. возможность перехода на новую строку), мы вычислим значение каждого ребра, которое является предыдущим недостатком.

Например, ниже 14 изображений, мы хотим, чтобы каждая строка была высотой 180 пикселей, а теперь ширина области просмотра составляет 1120 пикселей. Можно обнаружить, что существует 19 способов обертывания (19 сторон), которые в конечном итоге дадут 12 различных эффектов макета (12 путей). Синяя линия — наименее плохой путь (я бы не сказал, что лучший). Следуя этим краям, вы обнаружите, что нижний набор включает в себя все возможности макета, без повторяющихся строк и дублирующихся результатов макета.

Найти оптимальное решение для макета (или наилучшее возможное решение) так же просто, как найти кратчайший путь в графе.

К счастью, мы получаем ориентированный ациклический граф (DAG, без повторяющихся узлов в графе), так что вычисление кратчайшего пути можно выполнить за линейное время (что означает «быстро» для компьютера). Но на самом деле мы можем найти кратчайший путь при построении графа.

Чтобы получить общую длину пути, просто сложите вместе значения каждого ребра. Всякий раз, когда новое ребро появляется в том же узле, проверьте все пути, на которых оно находится, на предмет более короткого значения общей длины, и если да, запишите его.

Взяв в качестве примера 14 изображений выше, процесс проверки выглядит следующим образом: первая строка представляет текущее проиндексированное изображение (первое и последнее изображение в строке), следующее изображение представляет найденную точку разрыва строки и связанные края. подключен, кратчайший путь на текущем узле отмечен розовым цветом. Это вариант приведенной выше диаграммы — каждое ребро между прямоугольниками связано с уникальным расположением строк.

Начиная с первой картинки и оглядываясь назад, если точка новой строки установлена ​​на индекс 2, множество здесь 114. Если вы устанавливаете новую точку на индекс 3, множество в это время становится 9483. Теперь нам нужно начать с этих двух индексов и найти следующую точку разрыва строк. Следующим этапом индекса 2 находится в 5 или 6. После расчета, обнаружено, что линия включает в 6, а путь короче (114 + 1442 = 1556). Следующим шагом на индексе 3 также может быть 6, но поскольку стоимость упаковки в 3 было слишком высокой, чтобы начать с того, что конечные недостатки в 6 были удивительно высокими (9483 + 1007 = 10490). Таким образом, текущий оптимальный путь состоит в том, чтобы укоренить при индексе 2, а затем индекс 6. В конце анимации вы увидите, что путь к индексу 11, выбранном в начале, не является оптимальным решением, то один на узле 8 есть.

Это продолжается до тех пор, пока не появится последняя картинка (индекс 13), после чего появится кратчайший путь, представляющий собой наилучшую схему компоновки (т. е. синий маршрут на картинке выше).

Левое изображение ниже — это традиционный алгоритм компоновки, а правое изображение — алгоритм оптимизации упаковки. Их идеальная высота строки составляет 180 пикселей.Присмотревшись, мы можем сделать два интересных вывода: традиционный алгоритм всегда будет сжимать высоту строки, а оптимизированный алгоритм смело увеличивает высоту строки. Конечным результатом действительно является то, что алгоритм оптимизации ближе к идеальной высоте.

После тестирования алгоритм FlexLayout (мы назвали его алгоритмом переноса изображения) действительно создает более идеальную сетку. Это приводит к более равномерной сетке (каждый ряд примерно одинаковой высоты), и в итоге средняя высота ряда будет ближе к заданной высоте. Поскольку FlexLayout будет учитывать различные варианты расположения и комбинации, также будут решения для крайних случаев, таких как панорамные фотографии. Если панорама сжата до очень короткой, плохое значение этого края будет высоким во FlexLayout, тогда оно точно не появится в конечном результате. Когда традиционный алгоритм встречает панорамную (сверхширокую) фотографию, он обрабатывает изображение как изображение в первой строке и чрезвычайно сжимает его, чтобы поместить в первую строку.

Это означает, что есть определенные строки, которые отличаются по высоте от заданной высоты, но не слишком сильно.

Есть много переменных, влияющих на конечный результат: количество изображений является одним из самых больших, также важны ширина области просмотра и коэффициент сжатия/растяжения.

Изображение выше — это то, что FlexLayout сгенерировал бы для реализации схемы макета из 25 изображений на узких, средних и широких экранах. Выбор точки разрыва строки на узком экране невелик, но количество генерируемых строк велико. По мере расширения экрана увеличивается возможность переноса точек на одну строку, соответственно уменьшается количество строк и уменьшается возможность компоновки.

По мере увеличения количества изображений количество схем компоновки будет расти в геометрической прогрессии. В окне просмотра средней ширины количество путей, соответствующих различному количеству изображений, выглядит следующим образом:

  1.  5 photos =         2 paths

  2. 10 photos =         5 paths

  3. 50 photos =     24136 paths

  4. 75 photos =    433144 paths

  5. 100 photos = 553389172 paths

скопировать код

Если изображений 1000, то компьютер не успевает считать количество вариантов компоновки, но магия в том, что он может сразу найти лучший путь, хотя и не успевает проверить, что путь действительно лучший .

Однако оптимальную компоновку можно рассчитать по формуле, вычисляется среднее значение вероятности точек разрыва строки для каждой строки, а затем вычисляется куб для вычисления общей возможности количества строк. Для большинства видовых экранов ширины может быть две или три схемы переноса строки на строку, и более пяти изображений могут быть размещены на одной строке. Обычно существует 2,5 ^ (количество изображений / 5) возможных макетов.

Возможны 100...000 (79 нулей) комбинаций из 1000 изображений, 10^100 возможных комбинаций из 1260 изображений.

Традиционный алгоритм может выводить только одну схему размещения за раз, в то время как алгоритм FlexLayout просчитывает триллионы и миллиарды схем одновременно и выбирает из них.наиболееодин из.

Вы, должно быть, задаетесь вопросом, может ли клиент/сервер обрабатывать такой огромный объем вычислений, конечно, ответ "конечно". Вычисление оптимального макета для 100 фотографий занимает 2 мс, 10 мс для 1000 фотографий, 50 мс для 10 000 фотографий... Мы также тестировали 1,5 секунды для 100 000 000 фотографий. Затраты времени традиционных алгоритмов в соответствующих сценариях составляют 2 миллисекунды, 3 миллисекунды, 30 миллисекунд и 400 миллисекунд соответственно.Хотя скорость выше, опыт не так хорош, как FlexLayout.

В начале мы просто хотели выбрать наиболее подходящий макет, а позже мы могли точно настроить интервал сетки, чтобы пользователь всегда видел лучший макет.

Все хвалят FlexLayout, также были реализованы версии для Android и iOS, теперь синхронно обновляются схемы реализации трех платформ, включая веб-версию.

Напоследок поделюсь хитростью.Каждый раздел будет просчитываться дважды: первый раз — одна фотография отрезка в разрезе, а размер — фото, второй раз — отрезок в разрезе, а размер — сегмент. Поскольку сегментов или картинок может быть слишком мало, одна строка не заполнена, поэтому требуется второй расчет, в это время алгоритм компоновки предложит объединить содержимое менее одной строки для достижения наилучшего визуального эффекта.

3. Достижение скорости прокрутки страницы 60 кадров в секунду

До сих пор мы провели большую оптимизацию для достижения наилучшего макета, но если браузер не может обработать такой объем данных, предыдущая работа будет потрачена впустую. К счастью, браузеры позволяют разработчикам оптимизировать отображение страниц.

За исключением загрузки первой страницы, пользователи часто испытывают «медленность» при манипулировании страницами, особенно при прокрутке. Механизм браузера заключается в отрисовке 60 кадров в секунду (то есть 60 кадров в секунду), при такой скорости пользователь будет чувствовать, что страница работает очень плавно, в противном случае он будет чувствовать себя застрявшим.

Что значит 60 кадров в секунду? То есть время рендеринга на кадр не может превышать 16 миллисекунд (1/60). Но помимо рендеринга содержимого страницы у браузера есть много задач — обработка событий, анализ стилей, вычисление макета, преобразование всех единиц измерения элементов в пиксели и, наконец, отрисовка — оставляя не менее 10 миллисекунд.

В эти драгоценные 10 миллисекунд необходимо убедиться, что эти задачи выполняются эффективно и не тратится время впустую.

Сохраняйте размер DOM одинаковым

Слишком много элементов повлияет на производительность страницы. Есть две основные причины: во-первых, браузер занимает слишком много памяти (1000 изображений по 50 КБ требуют 50 МБ памяти, а 10 000 изображений занимают 0,5 ГБ памяти, чего достаточно для сбоя Chrome). ); Да, чем больше элементов, тем больше работы со стилями, компоновкой и композицией приходится выполнять браузеру.

Несмотря на то, что пользователи сохранили тысячи изображений в Google Фото, они могут видеть только один экран за раз.В большинстве случаев на одном экране могут отображаться только десятки изображений.

Мы считаем, что нужно не загружать сразу все картинки на страницу, а следить за работой пользователя на странице, а затем отображать картинки на соответствующей позиции при прокрутке страницы.

Некоторые изображения, которые были видны раньше, теперь перемещаются за пределы области просмотра из-за прокрутки страницы, поэтому уберите их.

Даже если пользователь просмотрел сотни или тысячи фотографий на странице, из-за ограничения области просмотра каждый раз необходимо отображать не более 50 изображений. При такой стратегии на взаимодействие с пользователем всегда можно реагировать своевременно, а браузеры не подвержены сбоям.

К счастью, картины заранее сгруппированы по размерам сегмента и секции, и теперь нет необходимости работать с одной картинкой, а весь модуль можно монтировать/подвешивать за один раз.

переменная минимизация

В Google Developers есть много хороших статей, в которых рассказывается о производительности рендеринга, а также множество руководств о том, как использовать встроенные инструменты мониторинга производительности в Chrome. Здесь я кратко расскажу о некоторых методах, используемых в Google Фото, а для получения более подробной информации посетите Google Developers. Во-первых, давайте разберемся с жизненным циклом рендеринга страницы:

Всякий раз, когда страница изменяется (обычно инициируется JS, но есть также сценарии, запускаемые стилями или анимацией), браузер сначала подтверждает, какие из них изменены.стильполученные изменения, пересчитать элементмакет(размер и положение), затем повторнорисоватьВсе затронутые элементы (например, преобразование текста, изображений... в пиксели). Чтобы повысить эффективность обновления содержимого страницы, браузеры обычно делят элементы на разные слои и рисуют их в единицах слоев.синтез.

По большей части браузеры настолько умны, что вы, вероятно, даже не помните этот конвейер рендеринга. Но если содержимое страницы меняется слишком часто (например, при постоянном добавлении/уменьшении изображений), то будьте осторожны.

Чтобы свести к минимуму изменения на странице, мы позиционируем все дочерние элементы относительно их родительских элементов. Разделы абсолютно позиционируются на всей компоновке сетки, а сегменты абсолютно позиционируются относительно раздела, в котором они находятся. И так далее, изображение абсолютно позиционируется в том сегменте, к которому оно принадлежит.

После позиционирования всех элементов, когда нам нужно изменить размер секции (фактическая высота и предполагаемая высота часто отличаются, такое обновление будет происходить), всем элементам под их физическим положением нужно только изменить верхнее значение Вот и все. Этот макет позволяет избежать множества ненужных обновлений DOM.

Свойство CSS contains определяет, насколько независимым является элемент, поэтому браузер знает, насколько этот элемент влияет на остальную часть контекста. Поэтому мы добавляем этот атрибут и в раздел, и в сегмент:

  1. /* 元素内外部内容不会相互影响 */

  2. contain: layout;

скопировать код

Есть также некоторые проблемы с производительностью, с которыми проще справиться, например, события прокрутки будут запускаться несколько раз в одном кадре, а прокрутка также будет запускаться непрерывно при увеличении окна браузера. Если макет постоянно меняется, браузеру не нужно пересчитывать стили и макеты при внесении первых изменений.

К счастью, это поведение по умолчанию можно отключить с помощью window.requestAnimationFrame(callback) , которая выполняет функцию обратного вызова перед появлением следующего кадра. При обработке событий прокрутки и масштабирования мы можем использовать его для выполнения функций обратного вызова вместо непосредственного обновления макета; масштабирование окна делает нечто более сложное: через полсекунды после того, как пользователь определил окончательный размер окна, а затем выполняет обновление.

Второй распространенной проблемой является дрожание макета. Когда браузеру необходимо вычислить макет, он сначала кэширует макет, чтобы информацию о ширине, высоте и макете элемента можно было быстро найти позже. Однако, как только атрибуты, которые могут повлиять на макет, изменены (например, атрибуты позиционирования ширины и высоты, сверху или слева...), предыдущий кеш макета сразу становится недействительным; когда атрибуты макета считываются снова, браузер принудительно пересчитает макет (один и тот же кадр. Таких повторных вычислений будет много).

Проблемы могут возникнуть в сценариях с большим количеством элементов в циклическом макете (например, сотни изображений). Чтобы прочитать атрибут макета, измените макет (переместите изображение или раздел в правильное положение), а затем прочитайте атрибут макета, чтобы запустить новый раунд расчета макета.

Простое решение может избежать вышеуказанной проблемы: прочитать все значения сразу и обновить их все сразу (то есть разделить чтение и запись и выполнить пакетную обработку). Однако наш подход заключается в том, чтобы не читать значения, а записывать размер и положение каждой фотографии и позиционировать их абсолютно. Когда происходит прокрутка или масштабирование окна, мы выполняем все расчеты на основе записанной информации о фотографии. Этот метод обновления не вызывает дрожания. На следующем рисунке показана производительность при прокрутке и обновлении страницы на один кадр (видно, что в конвейере рендеринга нет повторяющихся ссылок):

Избегайте непрерывного выполнения кода

Из-за появления Web Workers и поддержки нативных асинхронных методов (таких как Fetch) вкладка имеет только один поток, то есть код в одной вкладке выполняется в одном потоке — включая рендеринг и JS. Это означает, что если есть код (например, метод длительной прокрутки), который блокирует рендеринг страницы, работоспособность пользователя будет крайне плохой.

Самой трудоемкой частью нашего решения было создание макетов и элементов. Эти две операции должны быть выполнены в течение определенного периода времени, чтобы пользователь не пострадал.

Например, макет из 1000 изображений занимает 10 миллисекунд, а 10 000 изображений — 50 миллисекунд, что занимает 60 миллисекунд времени обновления. Но поскольку мы делим изображение на разделы и сегменты, для обновления сотен изображений за раз требуется всего 2–3 миллисекунды.

Самое «дорогое» событие компоновки — это масштабирование окна — размер каждой секции нужно менять. Мы просто вернулись к исходному алгоритму — даже если некоторые разделы уже загружены, мы этого не делаем, а используем только алгоритм FlexLayout для разделов в видимой позиции. Подождите, пока другие разделы не будут прокручены до границ окна просмотра, прежде чем пересчитывать.

Та же логика используется при создании элементов — мы делаем расчеты макета только перед тем, как изображение будет видно.

результат

Сделав так много, мы, наконец, получили приличную компоновку — по большей части 60 кадров в секунду, хотя иногда случались просадки кадров.

Пропущенные фреймы обычно случаются в основных сценариях макета (например, при вставке совершенно нового раздела) или когда браузер хочет переработать особенно старые элементы.

4. Чувство момента

Я полагаю, что большинство интерфейсных инженеров потратят много времени на пользовательский интерфейс, чтобы продемонстрировать свои навыки, например, наложение некоторых специальных эффектов на фейерверк.

Один из моих любимых треков «Будь осторожен» придумал коллега по YouTube. Когда они обрабатывают индикатор выполнения (красная полоса в верхней части страницы), они не используют реальный прогресс загрузки страницы (в то время нет точной информации о ходе), а используют анимацию для имитации «загрузки» до тех пор, пока Когда страница действительно загружается, красная линия достигает крайнего правого края. Я не уверен, что теперь YouTube сопоставляет анимацию загрузки с фактическим процессом загрузки страницы, но общая идея такова.

Точность прогресса загрузки второстепенна, и самое главное — дать пользователю почувствовать, что прогресс этой страницы движется вперед.

В этом разделе я поделюсь некоторыми приемами, позволяющими сделать Google Фото более плавным (больше, чем на самом деле) — большинство приемов связаны с загрузкой изображений.

Первое, и, вероятно, самое эффективное, это то, что контент, который пользователь, скорее всего, увидит, загружается первым.

После загрузки изображений в диапазоне области просмотра будет загружен дополнительный экран изображений, чтобы гарантировать, что при следующем прокрутке страницы пользователь сразу же увидит новое изображение.

А вот для экранов HDPI (где нам нужно загружать миниатюры большего размера) сложнее реагировать на все запросы при быстрой прокрутке.

Поэтому мы оптимизировали схему загрузки — сначала загрузите изображения-заполнители на следующих четырех или пяти экранах, эти изображения часто очень маленькие, поэтому их можно загрузить сразу. Когда эти изображения будут перемещены в область просмотра, загрузите исходные изображения.

Это означает, что если пользователь медленно прокручивает страницу с нормальной скоростью для просмотра картинок, он не увидит процесс загрузки фотографий за пределами области просмотра; но также бывают сценарии, когда пользователь быстро прокручивает страницу, чтобы найти определенную картинку. , то то, что увидит пользователь, это будет миниатюра картинки, и будет ощущаться общая информация.

Всегда будет выполняться ненужная работа для извлечения содержимого страницы, но в то же время для обеспечения бесперебойного взаимодействия с пользователем, что представляет собой сложную игру компромиссов.

Мы учитывали следующие факторы. Первое, что нужно проверить, — это направление прокрутки страницы. Контент, который пользователь собирается увидеть, должен быть предварительно загружен; он также определит, следует ли загружать исходное изображение высокой четкости в зависимости от скорости, с которой пользователь прокручивает страницу. В этом нет необходимости, даже если страница прокручивается так быстро, что даже заполнители с низким разрешением не нужно загружать.

Независимо от того, загружается ли исходное изображение или изображение-заполнитель с низким разрешением, будут сцены масштабирования изображения. Текущие дисплеи в основном являются экранами высокой четкости.Обычная практика заключается в том, чтобы загрузить изображение, вдвое превышающее размер отпечатка, а затем уменьшить его вдвое и поместить в соответствующее положение (таким образом, один пиксель фактически может нести дважды количество информации). Для карт мест с низким разрешением мы можем запросить очень маленькие ресурсы с высоким сжатием (скажем, 75% сжатия), а затем масштабировать их.

Возьмем в качестве примера этого спящего леопарда. Изображение слева — это то, что мы увидим после того, как оно полностью загрузится в макете сетки (оно было уменьшено до половины размера фактического изображения), а изображение справа — низкое Изображение-заполнитель с разрешением (также увеличенное до размера заполнителя), которое видно, когда пользователь быстро проводит пальцем.

Также обратите внимание на размер файла изображения. Сжатая миниатюра в высоком разрешении составляет 71,2 КБ, а замещающее изображение в низком разрешении — 889 байт после того же алгоритма сжатия, что составляет лишь 1/80 исходного изображения в высоком разрешении. изображение! Преобразовывая его, первые четыре страницы карты трафика исходного изображения высокой четкости.

В обмен на лучший пользовательский опыт с небольшим увеличением трафика карты заполнитель может позволить пользователям чувствовать богатство содержимого веб-страницы, а также предоставляют визуальную ссылку при просмотре.

Последнее, что нужно учитывать, — это то, как браузер отображает низкие разрешения. По умолчанию, когда маленькое изображение большое, браузер выполнит плавную обработку пикселей (средняя часть показана ниже), но визуальный эффект не очень хороший. Если вы используете размытие, чтобы справиться с ним (крайний справа справа внизу), будет намного лучше. Однако фильтр практически не влияет на производительность страницы: если к сотням картинок добавить фрагмент изображения, производительность страницы будет невообразимой. Поэтому мы выбрали другой путь, чтобы позволить браузеру обрабатывать эти изображения попиксельно, но я не уверен, что текущий Google Фото все еще использует это решение, эта часть была пересмотрена.

Если вы хотите, чтобы пользователи никогда не видели изображения с низким разрешением (за исключением таких сцен, как быстрая прокрутка, которые неизбежны), особенно когда вы собираетесь войти в область просмотра, исходное изображение высокой четкости заменит временное соединение изображения-заполнителя. Раньше мы использовали Animate для завершения этого перехода (чтобы избежать слишком навязчивой прямой замены изображения). Конкретная реализация заключается в наложении изображения-заполнителя и исходного изображения, а когда необходимо отобразить исходное изображение, изображение-заполнитель постепенно изменяется от непрозрачного до полностью прозрачного — один из распространенных методов перехода, и статья в Medium также отображается таким образом. Google Фото, возможно, удалили эту логику перехода, но процесс перехода от пустой сетки к содержимому может по-прежнему использовать этот эффект.

Такой визуальный опыт заставит пользователя почувствовать, что изображение загружается, а анимация длится 100 миллисекунд — достаточно, чтобы за это время загрузить исходное изображение.Для вашего удобства следующее изображение представляет собой медленную анимацию:

Этот трюк также используется в другом месте: миниатюры расширяются до полноэкранных превью. Когда пользователь нажимает на миниатюру, мы сразу же начинаем загружать исходное изображение. В ожидании исходного изображения мы увеличиваем миниатюру и располагаем ее в середине экрана. Когда исходное изображение загружается, мы отображаем исходное изображение, изменяя прозрачность. В отличие от загрузки миниатюр, на этот раз требуется манипулировать только одним изображением, поэтому используется фильтр размытия (пиксельный опыт определенно не сравним с эффектом размытия).

Будь то прокрутка изображений или переключение между режимом эскизов и режимом полноэкранного предварительного просмотра, мы всегда хотим, чтобы пользователь чувствовал, что браузер усердно работает над задачей, даже если конечный результат еще не готов. Противоположность этой философии взаимодействия заключается в том, что когда пользователь нажимает на миниатюру, не происходит обратной связи или даже появляется пустой экран до тех пор, пока исходное изображение не будет полностью загружено.

Пустые разделы также используют эту идею. Наш макет сетки будет загружать раздел только тогда, когда его нужно отобразить (есть также некоторые предварительно загруженные изображения). Если пользователь перетащит полосу прокрутки напрямую, он увидит раздел, который еще не был загружен.Хотя место было зарезервировано, когда пользователь просматривает эту позицию, он понятия не имеет, какое изображение и макет он увидит.Подготовьтесь.

Чтобы прокрутить опыт более естественным, мы устанавливаем высоту этих зарезервированных сечений к росту целевой строки и заполните их цветами для обозначения заполнителей. В начале загрузки раздел выглядит как длинный серый прямоугольник (самый левый на рисунке ниже), и недавно она была пересмотрена, чтобы иметь строки и столбцы, такие как самые главные на рисунке ниже, что ближе к изображению. В середине рисунка ниже показан раздел, который был загружен, но изображение еще не отображается.

Этот процесс загрузки изображения похож на отслеживание треков животных, поэтому попробуйте идентифицировать эти состояния в следующий раз, когда вы используете Google Photos.

Цветовой блок-заполнитель раздела реализован не с помощью изображений, а с помощью CSS, поэтому даже если ширина и высота будут изменены по желанию, деформации или обрезки не будет:

                                                                                                                                                                                                            
  1. /* 在 section 加载好之前,占位的宽高比是 4:3 */

  2. background -color : #eee;

  3. background -image :

  4.    linear -gradient (90deg , #fff 0, transparent 0, transparent 294px, #fff 294px, #fff),

  5.    linear -gradient (0deg ,   #fff 0, transparent 0, transparent 220px, #fff 220px, #fff);

  6. background -size : 298px 224px;

  7. background -position : 0 0, 0 - 4px ;

скопировать код

Кроме того, у нас есть множество мелких хитростей, большинство из которых связано с оптимизацией порядка запросов. Например, вместо того, чтобы запрашивать сразу 100 миниатюр, мы делим их на 10 пакетов и запрашиваем по 10 за раз. Так что если пользователь вдруг начнет быстро прокручивать страницу, трафик следующих 90 листов не будет потрачен впустую. Аналогичная логика также всегда отдает приоритет запросам изображений в пределах области просмотра, слегка — изображениям за пределами области просмотра и т. д.

Мы даже повторно используем эскизы одинакового размера — например, после того, как пользователь увеличивает окно, макет сетки существенно не меняется, меняется только количество строк. В этом случае мы не будем повторно загружать миниатюру другого размера, а масштабируем существующее изображение и повторно запрашиваем изображение только тогда, когда размер окна полностью изменится.

В заключение

Google Фото учитывает множество деталей взаимодействия с пользователем, а сетка — это лишь верхушка айсберга.

На первый взгляд это выглядит как простой или даже статический макет, но на самом деле сетка постоянно меняется в режиме реального времени — загрузка, предварительная выборка, анимация, создание, удаление... Делая все возможное, чтобы предоставить пользователю наилучший опыт.

Команда всегда отдает приоритет обеспечению и улучшению производительности продукта. Команда Google Фото отслеживает работу продукта в режиме реального времени с помощью таких показателей, как скользящая частота кадров, частота загрузки модулей и т. д. Google Фото всегда движется вперед.

Ниже приведена запись экрана прокрутки страницы Google Фото. Когда пользователи медленно просматривают страницу, они могут видеть четкие эскизы; когда скорость прокрутки увеличивается, они видят пиксельные изображения-заполнители, а когда они снова возвращаются к медленной прокрутке, снова отображаются изображения высокой четкости; то, что вы видите, является серым цветовым блоком-заполнителем. Различные скорости прокрутки загружаются по-разному (https://www.youtube.com/watch?v=AEpwAzLISXU):

Спасибо Винсенту Мо, моему руководителю в Google Фото, который оказал мне большую поддержку и сделал все фотографии, использованные в этой статье (также использованные Винсентом на этапе тестирования продукта). Спасибо Джереми Селиеру, руководителю веб-сайта Google Фото, который сейчас возглавляет команду по поддержке и улучшению веб-интерфейса Google Фото.