FRESH! Client Side Performance Improvement

sutiwo♨
10 min readApr 21, 2017

--

Hi, everyone, I’m a front engineer working for FRESH! at CyberAgent, Inc. FRESH! is an Internet broadcast platform in Japan.

Today, we will inform you about the efforts and results of improving the performance of browsers on smartphones and PCs.

At first, We discussed and decided the aims for improving the performance.

discussing

After discussing, we decided on the following:

  1. Asset caching with Service Worker
  2. Improving mobile site for Lazy Load with Intersection Observer
  3. Using HTTP2 and Progressive SVGs instead of SVG sprites

Next, we explain each section.

Asset caching with Service Worker

What is a Service Worker

It is a type of Web Worker that runs in the background separately from the Web page.
In addition to handling network processing, we also implemented functions that have long been demanded on the Web, such as push notification reception and background synchronization.
Some of the features and implementation notes are as follows.

  • You cannot perform DOM operations because it is a kind of Web Worker. Communicating with the browser is done using postMessages.
  • Only https, exception localhost (when in development)

This time, we used fetch events, which are one of the functions of a Service Worker.
When requesting, we check if there is a cache in the Service Worker, if there is a local cache use it, otherwise proceed with the external request.

Design policy and method

We prepared two types of caches.
First is cached by release for the static assets from the docker build time stamp. (Including Web Fonts, CSS Files, SVG Icons)
(v1490957780 from the image below)

Second, the version remains unchanged unless we intentionally update it. (assets.json)
(v20170321 from the image below)

We also separated it into three files to make management easier and put the built service-worker.js file into the root directory.

service-worker
├── assets.js
├── index.js
└── register.js

assets.js is a whitelist for how files (Web Font, CSS and JS Files) should be managed.
index.js is registered eventHandlers.
register.js determines if the Service Worker is installed or not.

/**
* index.js
*/
import { VENDORS, FONTS, ASSETS } from './assets';
// time stamp of CircleCI via environment variable as build_id (depend on release, Weaker Cache) *1
const assets = require('../../assets');
// Unchanged version unless we intentionally update it (Stronger Cache) *2
const version = require('../../version');
const CACHE_KEYS = [
`fresh-vendor-v${version.vendors}`,
`fresh-fonts-v${version.fonts}`,
`fresh-assets-v${assets.build_id}`
];
self.addEventListener('install', e => {
e.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', e => {
// Delete not matched CACHE_KEYS when activated
const deletion = caches.keys()
.then(keys => keys.filter(key => CACHE_KEYS.indexOf(key) === -1))
.then(keys => Promise.all(keys.map(key => caches.delete(key))));
e.waitUntil(deletion.then(() => self.clients.claim()));
});
self.addEventListener('fetch', e => {
const url = e.request.url;
// Files that are not covered fallback by early return
if (!VENDORS.some(file => url.includes(file)) &&
!FONTS.some(file => url.includes(file)) &&
!ASSETS.some(file => url.includes(file))) {
return;
}
// Find cache from storages
const cache = caches.match(e.request).then(response => {
// if find, return it as is browser
if (response) {
return response;
}
// If not found, do request
return fetch(e.request.clone()).then(response => {
if (response.ok) {
const clone = response.clone();
const cacheKey = getCacheKey(url);
caches.open(cacheKey).then(cache => cache.put(e.request, clone));
}
return response;
});
});
e.respondWith(cache);
});

With that said, when you visit for the first time, the target file will be cached. Then, when you visit a second time, local files via proxy will be referenced.

How to debug

When introducing Service Workers, it is necessary to confirm their installation, and that target static assets are cached as intended.
This can be easily checked using Chrome DevTools.

The Service Workers pane in the Application panel

If you click on Service Workers in the Application tab, you can confirm the status in visited site scope.
Like shown in the above image, the status indicator light is green, meaning service-worker.js is available.

By the way, if you check “Show All” upper right, the registered Service Worker of the visited site will be displayed in the list.
It is also interesting to see which sites use Service Workers among you visited sites.

The Cache Storage pane in the Application tab

Cache Storage in the Application panel is able to open and close. You can check files cached by Service Worker by clicking.The request files are displayed according to `CACHE_KEY _ *` which is divided by the above code.

Headers in Network tab (narrowed the files with Regex)

Headers in Network tab show that the Status Code of the requested libs.js is 200. In addition, “(from Service Worker)” is displayed. This is evidence that request are being proxied via cache and not http.

We briefly explained how to debug Chrome DevTools, but

Debug Progressive Web Apps | Web | Google Developers
explains in more detail. If you are interested, please have a look.

Compare results

The above video compares the transition from Google search on an iPhone7 Safari (left screen) without Service Worker and a Nexus 5X Chrome (right screen) with Service Worker.

We used a 3G carrier line for each result so that the results are clear. Starting measurement immediately after the touch end on the search screen, the time is stopped at the point where the hero image was displayed.

As shown in the video, if it is not compatible with Service Worker, it took 4.22 seconds to display the hero image, while Nexus 5X consumed only 2.10 seconds.
The figures are approximate median values measured several times.

Not only the hero image, but also in the first view of this page, there are many images cached using the Service Worker, such as SVG icons and store download images (App Store, Play Store), and you can see that the display is fast overall.

Let’s throttle to Good 3g and actually check with devTools to see how long it will take for the hero image to appear.

Timing the hero image with Service Worker
Timing of the hero image when requesting from CDN

Although it is obvious when comparing the two images, the time of Content Download is 0.00066 seconds (best effort in measuring several times) when using Service Worker, and 1.47 seconds when actually downloading it.

The size of the hero image this time is about 50 kb, but if the file size is bigger there will be more noticeable difference.

Of course, these results will change depending on the network speed and the performance of the CPU of the smartphones, so please use it as a guide only.

Improving mobile sites with Lazy Load using Intersection Observer

What is Intersection Observer

Intersection Observer is an API that allows you to easily determine whether a particular DOM element is in the screen and its actual position.

Compared to the conventional method described later, it was possible to efficiently detect the appearance of DOM elements by scrolling.

Why use it

FRESH! uses popular list type and feed type layouts on smart phones.

As a disadvantage, displaying many lists during initial rendering will inevitably increase the number of requests for thumbnails.

If these thumbnail images are asynchronously loaded with scrolling as a trigger, it will lead to the reduction of unnecessary requests and it will be a great advantage. (There is also the possibility that the user will transition away after only seeing the first view.)

However, if you implement such a UI without using Intersection Observer, you need to thin out scroll events so that they do not occur frequently.

At the same time, there was also the disadvantage that forced layout synchronization (forcibly executing the layout process in order to acquire the latest layout information) will occur in the scrollTop, offset, getBoundingClientRect() used to judge where the element is.

Therefore, in that case, it is important to adjust the execution interval by throttling or debouncing.

For details, please see What forces layout/reflow. The comprehensive list..

Design policy and method

We incorporate the idea of rendering Isomorphic(SSR + SPA) with React as an overall architecture for FRESH! Web applications.

For details, please see Isomorphic Architecture を実装してるときの細かいアレコレ ::ハブろぐ Sorry Japanese Only — but is about fluxible, Configuration, UserAgent etc…

Image/index.js is responsible for the image rendering part of the UI component, and if the browser supports Intersection Observer within componentDidMount, it creates an instance and starts monitoring crossover processing.

The point is to set the rootMargin option.

By specifying rootMargin as the second argument, you can capture the image shortly before reaching the display area and provide the user with an experience as if the image had already been loaded.

Incidentally, the setting of rootMargin is the same as CSS’s margin omission method, and in the following cases it will be 200 px up and down and 0px left and right.

After requesting the image, use React’s setState to update the component so that it can be rendered.

/**
* Image/index.js
*/
'use strict';
const BLANK_IMAGE = 'data:image/gif;base64,=';
const ROOT_MARGIN = '200px 0px';
export default class Image extends React.Component {
static propTypes = {
src : React.PropTypes.string
};
state = { src : BLANK_IMAGE }; intersectionObserver; startObserve() {
this.intersectionObserver = new IntersectionObserver(entries => {
if (this.state.src === BLANK_IMAGE) {
this.intersectionObserver.unobserve(this.refs.img);
this.setState({ src : this.props.src });
}
}, {
rootMargin : ROOT_MARGIN
});
this.intersectionObserver.observe(this.refs.img);
}
componentDidMount() {
this.startObserve();
}
componentWillReceiveProps(nextProps) {
if (nextProps.src !== this.props.src) {
this.setState({ src : nextProps.src });
}
}
componentWillUnmount() {
if (this.intersectionObserver) {
this.intersectionObserver.unobserve(this.refs.img);
this.intersectionObserver = null;
}
}
render() {
return (
<img src={this.props.src} />
);
}
}

Actually, in addition to the above code, processing such as adding image query parameters (width, height, crop, pixelRatio) to CDN, and handling at the time of image error, giving an image alt etc. are added.

Based on the fact that it is only effective in Chrome for Android for smartphones, we have adopted a policy to use official Polyfill instead of progressive enhancement.

The results

The images are being loading asynchronously by scrolling, but you can see that rendering is really smooth thanks to rootMargin.

The screencast when scrolling
BEFOREAFTERCONTENT REQUEST from 80 to 40
CONTENT SIZES from 1,500kb to 900kb

With quantitative figures, CONTENT REQUEST and CONTENT SIZES could both be reduced in half.
Since the Fonts file has been moved from Google Fonts to its own CDN for caching with the Service Worker, the font size is increasing.

By avoiding unnecessary requests and quickly performing the initial display, benefits were created for both the end user and the service side.

Using HTTP2 and Progressive SVGs instead of SVG sprites

SVG sprites and HTTP 2

Until now, the icons were using combined SVG files.

However, since the ALB (Application Load Balancer) used in FRESH! Is compliant with HTTP/2, we were able to reduce the cost of the request which was an issue in HTTP/1.1 by using communication multiplexing and concurrent requests.

Therefore, the advantages of combining SVG files are reduced.
Rather, if you combine sprites into a single file, it was not rendered until the whole file downloaded, making the icons display late.

The Results

By not using sprite SVGs, the browser now starts to render as soon as it downloads the SVG file of each icon.
As a result, important icons such as the service logo at the top of the screen and icons within the playback button of the viewing page are not displayed late.

Summary

With the efforts of this performance improvement both the speed of Start Render and Speed Index in Speed Curve became about 1 second faster before and after release.
In the image below, you can see that the graph is showing a decrease with 20160316_01 as the boundary.

Change in RENDERING

We are very glad to be able to incorporate these kind of functional (very meaningful) improvements into the service we are using daily.

What made these improvements go so smoothly is the fact that FRESH! is a relatively new service, being in a modern environment. (Service Worker requiring HTTPS etc.)

Comparison with other video services

From here on, while decreasing the Speed Index score which is a measure of performance improvement over the long term, we also as a team aim to continue development while pursuing a better understanding of the web eco-system.
So that users are able to say that “FRESH! is fast and easy to use!” compared to other services that they are used to, we plan to improve the rendering speed at SSR even if it is just in 0.01 second steps.

Finally, we hope this article ends up being a little reference for the projects you are working on daily.

FRESH!(フレッシュ) — 生放送がログイン不要・高画質で見放題

--

--