The Next-Gen Web

A case study of how we

re-built Flipkart.com

Abhinav Rastogi
@_abhinavrastogi

const async = bundle => (location, cb) => {
	bundle(component => {
		cb(null, component);
	});
};

const routes = {
    path: '/about',
    getComponent: (location, callback) => {
	require.ensure([], function (require) {
	    var Page = require('./Page.js');
	    callback(null, Page);
	});
    }
}

// or

const routes = {
    path: '/about',
    getComponent: async(require('bundle?lazy!./Page.js'))
}
<html>
    <head>
        <meta ...>
        <link rel="stylesheet" href="styles.css" />
        <link rel="preload" as="script" href="vendor.js" />
        <link rel="preload" as="script" href="chunk1.js" />
        <link rel="preload" as="script" href="chunk2.js" />
    </head>
    <body>
        <div>
            ... server rendered html ...
        </div>
        <script src="vendor.js"></script>
        <script src="chunk.1.js"></script>
        <script src="chunk.2.js"></script>
        <script src="chunk.3.js"></script>
    </body>
</html>

Flipkart Lite

SW, PWAs, App-shells

Probably the first

mobile -> desktop migration

Significant Differences

Mobile vs Desktop

Requirements

User Behaviour

Network Conditions

Device Capabilities

Browser Fragmentation

Typical SPA

  • Serve empty html from server

  • Client downloads JS bundle

  • Shows loaders

  • Makes API calls

  • Renders full page

  • Subsequent navigations are client-side

Pros

  • Easy

  • Fast navigations

  • Low server QPS

  • Cheap server processing

Cons

  • First paint is slow

  • SEO jeopardised

  • JS bundle is huge

What it looks like

HTML

CSS

JS download

JS Parse

API call

First Paint +

Full Render

Empty Page

Upgrade #1 - App Shells

  • Serve loading state html from server

  • Client downloads JS bundle

  • Makes API calls

  • Replaces loaders with content

  • Subsequent navigations are client-side

Pros

  • No empty page

  • Fast navigations

  • Low server QPS

  • Cheap server processing

Cons

  • Stuck on 'Loading'

  • SEO jeopardised

  • JS bundle is huge

What it looks like

HTML

CSS

JS download

JS Parse

API call

Render Content

First Paint + Render Loaders

Update #2 - Server Render

  • Render full page on server for first request

  • client renders full html

  • downloads JS

  • makes api calls

  • re-renders full page

Pros

  • First paint might improve

  • SEO is solved

Cons

  • Significant increase in server load

  • html download size increased

  • response time increased

  • JS file is still huge

Update #3 - Universal app

  • Render only seo-critical content on server

  • Client continues to work as normal

  • React is able to reconcile the DOM

Pros

  • Lesser load on server

  • Better SEO, smaller html

  • header/footer can load quicker

Cons

  • Still takes time to receive response

  • html is still large, it can be faster!

  • JS file is huge

  • user sees no organic content until JS loads

Update #4 - First-fold

  • Render above-the-fold content also on the server

Pros

  • Making API calls from server is better

  • user sees organic content in first paint

Cons

  • html is large

  • JS is large

  • generally sluggish experience

#5 - Route based chunking

  • Break JS bundle into parts based on routes. Only load JS reqd for current page

  • Webpack + react-router makes this easy using require.ensure

vs.

Route-based Chunking

Pros

  • smaller JS file for init

  • faster full page render

Cons

  • slightly increased complexity in build system

  • page navigations slower as JS needs to download on click

#6 - PRPL

  • Push critical resources for the initial route.

  • Render initial route.

  • Pre-cache remaining routes.

  • Lazy-load and create remaining routes on demand.

Pros

  • Instant page navigations

Cons

  • Need a smart way to predict next chunk. For most websites a static map can also work

  • html download is still taking time.

#7 - html streaming

  • Some parts of html are independent of page type/url. Send those first to start resource download

  • smart use of preload, prefetch and defer!

Streaming HTML

app.use(function (req, res) {
    res.set('Content-Type', 'text/html');
    let htmlHead = "<html><head><link><style>....<body>";
    res.write(htmlHead);
    res.flush();
    
    let htmlBody = ReactDOM.renderToString(<App />);
    res.write(htmlBody);
    res.flush();

    let scripts = "<script src='app.bundle.js'></script>";
    res.write(scripts);
    res.flush();
    res.end();
});

But wait, what about gzipping?

What it looks like

HTML

CSS

JS download

JS Parse

API call

Full

Render

Useful First Paint

<body>
<script>
	requestAnimationFrame(function () {
		performance.mark("first_paint");
	});
</script>
<div>
    ... all content ...
</div>

Pros

  • css and js resources can start downloading within milliseconds

  • page is ready to render as soon as html is downloaded

Cons

  • Server has to keep connection open with client until download completes

  • client can download limited number of resources at a time

Chunking + Streaming + Preload (PRPL)

Waiting for fonts - BAD!

var fontA = new FontFaceObserver('Roboto', { weight: 400 });
var fontB = new FontFaceObserver('Roboto', { weight: 500 });

Promise.all([fontA.load(), fontB.load()]).then(function () {
    document.documentElement.className += ' fonts-loaded';
});

Lazy-loading Fonts

body {
	font-size: 14px;
}

body {
	font-family: Arial, sans-serif;
	letter-spacing: -0.2px;
}

:global .fonts-loaded {
	body {
		font-family: Roboto, Arial, sans-serif;
		letter-spacing: 0;
	}
}

<video of fonts loading>

The Next-Gen Web

By Abhinav Rastogi

The Next-Gen Web

  • 924