How to Implement Google Photos Grid in JavaScript
Tags: webdev, javascript, html, tutorial
Published: 2021-02-12 13:59:08

This is a step by step tutorial of how I implemented Scrubbable Photos Grid in JavaScript.

Google Photos uses virtual scrubbable photos grid. Scrubbable photos grid is laying out all your photos in grid so that users can jump to any random year/month/day in their timeline. This grid is virtual so that we are efficiently using the user's resources like cpu, memory, disk and network.

In this blog post I will cover how to implement such a grid in JavaScript, whether we can substitute this with a simple navigation mechanism for jumping to random year/month/day is separate design discussion all together.

This implementation will use many techniques to make this virtual grid as close to actual grid including
* Estimating and maintaining grid height as close to actual
* Loading only visible photos to DOM
* Detaching not visible photos from DOM
* Positioning photos absolutely inside grid
* Adjusting scroll position to compensate layout shifts in some scenarios

<hr>

# Design

Throughout the blog I will give summary of design decisions Google has taken for their scrubbable grid, but I will recommend to check this [Google Design Blog](https://medium.com/google-design/google-photos-45b714dfbed1) for details.

>We are going to keep our design goals same as stated in Google Design Blog:
>* "**Scrubbable Photos** - the ability to quickly jump to any part of the photo library.
>* **Justified Layout** - fill the width of the browser and preserve the aspect-ratio of each photo (no square crops).
>* **60fps Scrolling** - ensuring the page remains responsive even when looking at many thousands of photos.
>* **Instantaneous Feel** - minimize the time waiting for anything to load."<br>

Google has gone extra mile to ensure row heights are uniform and near the target height while implementing justified layout, we will just use Flicker's justified layout lib for this demo.

<hr>

# Implementation

I will be implementing this step by step, each step is a commit in the [scrubbable-photos-grid](https://github.com/shaileshpandit/scrubbable-photos-grid) Github Repo. If you want to skip all detailed explanation and check final implementation, check this [Glitch](https://glitch.com/edit/#!/scrubbable-photos-grid?path=script.js%3A15%3A0).

Our grid will be divided into sections, sections will have segments, segments will have tiles and tiles will have an image tag. Will cover reasons for these divisions in detail when we need to add them.
![Section Segments and Tiles](https://dev-to-uploads.s3.amazonaws.com/i/fg27c23lcwzbdcve5rhj.png)

I chose JavaScript to demonstrate how things should happen at runtime, you should be able to implement this in any client side framework of your choice.

<hr>

## Step 1 - Dividing whole grid in sections and estimating their heights

In contrast to pagination and infinite scrolling, scrubbable grid always has all the photos present in the grid giving correct representation of finite height and size of scroll knob. This also gives the user ability to jump to any random time in the whole timeline of photos instantly.

An inefficient implementation of this will load metadata like width and height of all photos in the whole timeline as soon as page loads. Using the width and height of each photo we can allocate space on page for actual height of the grid. This will waste a lot of bandwidth and initial load time will be unacceptable.

To reduce this initial metadata payload, we will divide the whole timeline into virtual sections and estimate the height of each section to get the estimated height of the grid. Our initial payload will consist of an array of all sections with a number of images in those sections. Simplest way to divide the whole grid into sections is to have a section per month in the timeline. So if users timeline spans 10 years, our initial payload will consist of max 120 sections and number of photos in each section.

We start by adding basic html with a `grid div` as a container of our  grid.

{% gist https://gist.github.com/shaileshpandit/3ca805ba77d0a164481e5cac642b7bc1 file=index.html %}

<br>Apis used are simulated in `api.js`, it basically depends on included `store.json` for all section details. We add the `getSections` api to retrieve all sections and count of images inside each section. Apis are using random latency of 50-550ms.

{% gist https://gist.github.com/shaileshpandit/3ca805ba77d0a164481e5cac642b7bc1 file=api.js %}

<br>Sample output of `getSections` api

{% gist https://gist.github.com/shaileshpandit/3ca805ba77d0a164481e5cac642b7bc1 file=getSections.js %}

<br>We add `script.js` for loading our grid. Our entry point is `loadUi`, in this we call `getSections` api. After getting all sections we allocate space by creating an empty `div` for each section with estimated height.

{% gist https://gist.github.com/shaileshpandit/3ca805ba77d0a164481e5cac642b7bc1 file=script.js %}

&nbsp;

>As described in Google Design Blog, to estimate height of sections we will assume average aspect ratio of all images being 3:2, approximate total width of all images in that section if laid horizontally and divide it by our grid width to approximate height of section when these images are wrapped at grid width.

{% gist https://gist.github.com/antinharasymiv/c1318e074b2832928cea68bb1e85f5f7 %}

<br>Next we add basic `style.css` to highlight sections
{% gist https://gist.github.com/shaileshpandit/3ca805ba77d0a164481e5cac642b7bc1 file=style.css %}

[Step 1 Full Commit](https://github.com/shaileshpandit/scrubbable-photos-grid/commit/7ddef4c73ee010f248a0235bddc3a80496ec4bb5)

<hr>

**Step 1 Result** - Observe that all empty section divs with estimated heights are created on load and give estimated height to the whole grid and scroll knob.
![Step 1](https://dev-to-uploads.s3.amazonaws.com/i/bsrtdeavyr84276gzyj4.gif)

<hr>

## Step 2 - Populating section with segments

While sections are virtual divisions of the whole grid to minimize initial load resources, segments are visible divisions of the grid for users to navigate and see photos in logical groups. We are going to use static segments for each day, but can be dynamic based on location or more granular time slots based on the number of photos a user has in a day.

We add `getSegments(sectionId)` api to retrieve all segments of a section and images inside each segment.
{% gist https://gist.github.com/shaileshpandit/b853ccf705ab5a364c298376af2bf790 file=api.js %}

<br>Here is a sample output of a `getSegments(sectionId)` call
{% gist https://gist.github.com/shaileshpandit/b853ccf705ab5a364c298376af2bf790 file=getSegments.js %}

<br>Next we add `populateSection(sectionDiv)` method in `script.js` to populate a section div. While populating a section div we call `getSegments(sectionId)` api, get segment html for all inner segments, add it to section div and update its height to 100% from the estimated height set initially.
{% gist https://gist.github.com/shaileshpandit/b853ccf705ab5a364c298376af2bf790 file=populateSection-script.js %}

<br>For generating segment html we use [justified-layout](https://www.npmjs.com/package/justified-layout) lib. It gives us an absolute layout for all the tiles inside the segment. We generate individual tile html using this absolute layout and add it as segment childs.
{% gist https://gist.github.com/shaileshpandit/b853ccf705ab5a364c298376af2bf790 file=getSegmentHtml-script.js %}

<br>We call `populateSection` eagerly in `populateGrid` for all sections to demonstrate how populated sections will look like in ui and in DOM.
{% gist https://gist.github.com/shaileshpandit/b853ccf705ab5a364c298376af2bf790 file=populateGrid-script.js %}

<br>Finally we make tiles absolutely positioned relative to segments and highlight segments and tiles in `style.css`
{% gist https://gist.github.com/shaileshpandit/b853ccf705ab5a364c298376af2bf790 file=style.css %}

[Step 2 Full Commit](https://github.com/shaileshpandit/scrubbable-photos-grid/commit/200db9fc752ae537c8f8d153148a27c5add6e0d4)

<hr>

**Step 2 Result** - Observe all sections and segments eagerly loaded.
![Step 2](https://dev-to-uploads.s3.amazonaws.com/i/hl2mghdchvvmc04udpns.gif)

<hr>

## Step 3 - Lazily load and unload sections

While in previous step we eagerly loaded all sections on page load for demo, we want to attach sections when they are about to come in viewport and detach when they go out of viewport. We will use [intersection observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to implement this.

First we create `sectionObserver` IntersectionObserver with `handleSectionIntersection` as the intersection handler and use `200px` of margin so that intersection will be triggered when our sections cross virtual viewport of actual viewport + 200px extra on both sides vertically.

{% gist https://gist.github.com/shaileshpandit/b86a26b5ec1f649d9971eae7b1c6d19a file=sectionObserver-script.js %}

<br>We handle intersection events by populating incoming sections and detaching outgoing sections from the virtual viewport. As populating a section needs fetching segments which is async, actual population can go out of order from intersection order. To mitigate this we maintain `lastSectionUpdateTimes` for all sections and only populate if this section was not updated meanwhile.

{% gist https://gist.github.com/shaileshpandit/b86a26b5ec1f649d9971eae7b1c6d19a file=handleSectionIntersection-script.js %}

<br>We detach the section by removing all child segments and not changing height.

{% gist https://gist.github.com/shaileshpandit/b86a26b5ec1f649d9971eae7b1c6d19a file=detachSection-script.js %}

<br>We attach `sectionObserver` to all sections instead of eagerly loading all sections in `populateGrid`.

{% gist https://gist.github.com/shaileshpandit/b86a26b5ec1f649d9971eae7b1c6d19a file=populateGrid-script.js %}

[Step 3 Full Commit](https://github.com/shaileshpandit/scrubbable-photos-grid/commit/b6cab98216964a789f250285cf725e91cad535a7)

<hr>

**Step 3 Result** - Observe how section divs are getting loaded and unloaded as we scroll.
![Step 3](https://dev-to-uploads.s3.amazonaws.com/i/z04umeikzxp2exgz6i6q.gif)

<hr>

## Step 4 - Moving segments and sections to absolute positioning

Ideally browsers will calculate positioning changes of all segments and sections efficiently when height of some section changes. But if we want to make sure we control all positioning changes, we can move both segments and sections to absolute positioning. Our sections will be positioned absolutely within the grid and segments will be positioned absolutely within sections. Our tiles are already positioned absolutely within segments.

First we set sections and segments to absolute positioning and remove margins in css.
{% gist https://gist.github.com/shaileshpandit/34788d32824c4a60e6fcfc4ca096ea35 file=style.css %}

<br>Next we maintain `top`, `height` and `lastUpdateTime` of all sections as a state in `sectionStates`. We initialize it in `populateGrid` and use it while creating initial detached sections.
{% gist https://gist.github.com/shaileshpandit/34788d32824c4a60e6fcfc4ca096ea35 file=populateGrid-script.js %}

<br>Next we update `populateSection` to generate segments with absolute positioning, calculating top of each segment. We calculate the new height of the section, check if it has changed, in case it is changed, we move all next sections by adding `heightDelta` to their tops. We also keep `sectionStates` in sync of these changes.
{% gist https://gist.github.com/shaileshpandit/34788d32824c4a60e6fcfc4ca096ea35 file=populateSection-script.js %}

<br>We don't need to keep the old height any more after detaching the section now, because absolute height remains the same after removing child segments.
{% gist https://gist.github.com/shaileshpandit/34788d32824c4a60e6fcfc4ca096ea35 file=detachSection-script.js %}

[Step 4 Full Commit](https://github.com/shaileshpandit/scrubbable-photos-grid/commit/ce0d430f51550ea6c69dd0a81cde9fcb5b5b8b26)

<hr>

**Step 4 Result** - Observe how absolute positioning is maintained by updating the top of all following section divs.
![Step 4](https://dev-to-uploads.s3.amazonaws.com/i/bu4sw6mx9bfstiya2v7y.gif)

<hr>

## Step 5 - Adjust scroll position in case of layout shift

At this point if you try to scroll down, sections will get attached and detached as expected and scrolling will feel normal. This works as the user scrolls linearly, sections get attached, their height changes, top of further sections increases, grid height changes, whole layout changes and yet we don't feel jumps while scrolling. This is because all layout changes are after current scroll position.

This will change if we allow random jumps to the scroll position. e.g. If we jump to a random scroll position on page load, we will be in state with some detached sections with estimated height prior to our current scroll position. Now if we scroll up, sections will get attached before scroll position and will result in layout changes before scroll position. It will feel like the whole page is jumping when we scroll. To try this just add the following to Step 4 commit inside `loadUi` and try scrolling up.
{% gist https://gist.github.com/shaileshpandit/c9a6edee3377e18f13e2575c4e6fd2f9 file=loadUi-script.js %}

<br>**Page Jumping** - Observe how it feels like page is jumping to new position while scrolling
![Page Jumping](https://dev-to-uploads.s3.amazonaws.com/i/sp8rhw5l0ii046gsk5n2.gif)


<br>To fix this, we check if our current scroll position is ahead of the section for which we adjusted height and adjust scroll by `heightDelta` at end of `populateSection`.
{% gist https://gist.github.com/shaileshpandit/c9a6edee3377e18f13e2575c4e6fd2f9 file=populateSection-script.js %}

[Step 5 Full Commit](https://github.com/shaileshpandit/scrubbable-photos-grid/commit/df9599074b011876b9b31b2466503d47b20e98ef)

<hr>

**Final Result** - Observe how page jumping is fixed by adjusting scroll position
![Final](https://dev-to-uploads.s3.amazonaws.com/i/jq46vi1o3gsmhgin8b8c.gif)

<hr>

**Final Glitch** - Open [live app](https://scrubbable-photos-grid.glitch.me). Use rewind in Glitch to see each step in action.
{% glitch scrubbable-photos-grid file=script.js %}

<hr>

# Whats next

There is lot to add to this, here are some things you can try:
* Add actual images.
* Go through google blog and add improvements they mentioned.
* Cancel ongoing api call to fetch segments when that section goes out of the virtual viewport to save bandwidth when the user is scrolling fast. We can even defer fetching when the speed of scrolling is high.
* Add intersection observers to tiles to load low-res thumbnails for distant tiles and high-res thumbnails for nearer ones.
* Add Google Photos like timeline instead of scroll knob, with this user will be able to jump to any year/month.
* Implement whole thing in some framework like React, you can create components for Section, Segment and Tile.
* This demo assumes that the grid consumes the whole viewport width, you can use grid container's width. It is currently not handling viewport resize also.
* One can even make open source component for scrubbable grid.

<hr>

# Resources

* Google Design Blog - [Building the Google Photos Web UI](https://medium.com/google-design/google-photos-45b714dfbed1)
* Flickr's Justified Layout Lib - [justified-layout npm](https://www.npmjs.com/package/justified-layout)
* Intersection Observer - [Intersection Observer Api - Web Apis - MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
* Github Repo for this implementation, with commits for each Step - [scrubbable-photos-grid](https://github.com/shaileshpandit/scrubbable-photos-grid)
* Live App with Source Code for this implementation - [Glitch](https://glitch.com/edit/#!/scrubbable-photos-grid?path=script.js%3A15%3A0)

<hr>

If you made it this far, kudos to you! I enjoyed implementing this and even more, writing about it. This is my first tech article in long time, so any feedback is much appretiated.

Also published in [JavaScript In Plain English on Medium](https://medium.com/javascript-in-plain-english/how-to-implement-google-photos-grid-in-javascript-6c294bcea001)