Slide 1

Slide 1 text

Fast By Default Modern Loading Best Practices

Slide 2

Slide 2 text

Bilal Çınarlı Frontend Architect Software Engineer @Adidas @bcinarli github.com/bcinarli bcinarli.com

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

First Paint First Meaningful Paint Time To Interactive USER-HAPPINESS

Slide 5

Slide 5 text

How to be fast? By following best practices also, investing in your code!

Slide 6

Slide 6 text

Mobile Web Delivery Best Practices Compress! (GZip, Brotli) Preresolve DNS Preload critical resources Cache effectively Respect Data Plans Stream HTML ⚡ Minify & Optimize *.* ✋ SSR is not a panacea ✂ Code-splitting Route-based chunking Library sharding PRPL Pattern Tree-shaking Differential serving / Scope Hoisting Don’t ship DEV to PROD *.js *.*

Slide 7

Slide 7 text

Improving your performance and applying best practices is a journey where small changes can lead to significant achievements

Slide 8

Slide 8 text

Being Progressive

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

You can load instantly in any network, even pre-caching resources to reduce the need for the network!

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

Become ridiculously fast! Once loaded, users expect pages to be fast.

Slide 13

Slide 13 text

INVESTMENTS IN SPEED

Slide 14

Slide 14 text

Where do mobile sites spend their time loading? With thanks to Camillo and Mathias @ V8

Slide 15

Slide 15 text

2017 JavaScript Parse Costs Average Phone ~1MB JavaScript (uncompressed)

Slide 16

Slide 16 text

JavaScript Parse Cost On Mobile - CNN ~9s difference to the A11

Slide 17

Slide 17 text

Follow Patterns

Slide 18

Slide 18 text

PRPL Pattern INSPIRED

Slide 19

Slide 19 text

Use Your Budget Wisely

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

https://twitter.com/kristoferbaxter/status/908144931125858304

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

Change Your Coding Style

Slide 26

Slide 26 text

WebPack is a module bundler which has lots of perks. Code Splitting, Chunking, Tree-shaking…

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Create a Lab Environment

Slide 29

Slide 29 text

Chrome DevTools Lighthouse WebPageTest

Slide 30

Slide 30 text

Use New Techniques Now!

Slide 31

Slide 31 text

If Web Fonts don’t load quickly, don’t load them at all.

Slide 32

Slide 32 text

@font-face { font-family: "Roboto"; font-display: optional; src: url("Roboto.woff") format("woff"), url("Roboto.eof") format("eot"); font-weight: 400; font-style: normal; }

Slide 33

Slide 33 text

I want to adapt serving based on network quality

Slide 34

Slide 34 text

// Network type that browser uses navigator.connection.type > ‘wifi’ // New: effective connection type // using rtt and downlink values navigator.connection.effectiveType > ‘2G’

Slide 35

Slide 35 text

I have a critical resource, I want to load earlier than the discovery

Slide 36

Slide 36 text

(async () => { try { const response = await fetch("movies.json"); const data = await response.json(); console.log(data); } catch (e) { console.log("Booo"); } })();

Slide 37

Slide 37 text

I want to start network requests while Service Worker is still booting up

Slide 38

Slide 38 text

addEventListener('activate', event => { event.waitUntil(async function() { // Feature-detect if (self.registration.navigationPreload) { // Enable navigation preloads! await self.registration.navigationPreload.enable(); } }()); });

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

0 15 30 45 60 Android iOS PWA 0,15MB 56MB 9,6MB Size: Comparing the PWA to the native apps 150KB home feed load

Slide 42

Slide 42 text

Comparing old mobile web to new mobile web +60% +44% +50% +40% Time Spent > 5 minutes User-generated Ad $ Ad Clickthroughs Core Engagements Comparing across web/native +2-3% +2% +0% +5% Time Spent > 5 minutes User-generated Ad $ Ad Clickthroughs Core Engagements

Slide 43

Slide 43 text

Old Mobile Site - 1st load First Paint: 4.2s First Meaningful Paint: 6.2s Time To Consistently Interactive: 23s

Slide 44

Slide 44 text

New Mobile Site - 1st load First Paint: 1.8s First Meaningful Paint: 5.1s Time To Consistently Interactive: 5.6s

Slide 45

Slide 45 text

New Mobile Site - Repeat Loads First Paint: 0.6s First Meaningful Paint: 3.5s Time To Consistently Interactive: 3.9s

Slide 46

Slide 46 text

Performance 20s ➡ 6.5s P90 for Pin pages 13s ➡ 4s P50 for Pin pages 620KB ➡ 150KB JS Bundles 150KB ➡ 6KB inline CSS Bundles

Slide 47

Slide 47 text

JavaScript Bundle Splitting Strategy How did Pinterest handle code-splitting? - Chunk 1: Vendor (react, redux, react-router etc) - Chunk 2: Entry (main shell, core logic, redux store) - Chunk 3: Async route chunks - Sizes: Vendor (~73KB), Entry (59KB), Async (13-18KB)

Slide 48

Slide 48 text

const chunkPlugins = [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor-mweb', minChunks: Infinity, chunks: ['entryChunk-mobile'] }), new webpack.optimize.CommonsChunkPlugin({ name: 'entryChunk-webpack', minChunks: Infinity, chunks: ['vendor-mweb'] }), new webpack.optimize.CommonsChunkPlugin({ children: true, name: 'entryChunk-mobile', minChunks: (module, count) => { return module.resource && (isCommonLib(resource) || count >= 3); } })]; Webpack Config const bundles = { 'vendor-mweb': [ 'app/mobile/polyfills.js', 'intl', 'normalizr', 'react-dom', 'react-redux', 'react-router-dom', 'react', 'redux' ], 'entryChunk-webpack': 'app/mobile/runtime.js', 'entryChunk-mobile': 'app/mobile/index.js' };

Slide 49

Slide 49 text

// Create a loader const Closeup = () => import(/* webpackChunkName: "CloseupPage" */ 'app/mobile/routes/CloseupPage'); // Register it to the route route('/pin/:pinId', routes.Closeup, {name: 'Closeup'}); // Render a react-router-v4 Route with the route bundle loader }/> React Router V4

Slide 50

Slide 50 text

// Async load the route bundle class PageRoute extends PureComponent { render() { const {bundleLoader, ...props} = this.props; return ; } } // Load it and render class Loader extends PureComponent { componentWillMount() { this.props.loader().then(module => { this.setState({LoadedComponent: module.default}); }); } } React Router V4

Slide 51

Slide 51 text

Webpack Bundle Analysis Using webpack-bundle-analyzer

Slide 52

Slide 52 text

Duplicate code in the async chunk Tackling dupes across lazily loaded routes - Moved common code into the Entry chunk - 20% increase in size of Entry (59KB -> 71KB) - 60-90% decrease in size of async route chunks - E.g: Homefeed (13.9KB -> 1KB), Closeup (18KB -> 7KB

Slide 53

Slide 53 text

Before splitting out common async route code

Slide 54

Slide 54 text

After moving out common code from async chunks into entryChunk:

Slide 55

Slide 55 text

Service Worker Caching Journey Caching runtime & static assets offline Start - Runtime caching async JS chunks - Precaching vendor & entry chunks - Precaching most used routes (e.g Pins) - Generating a SW for each locale bundle Today - Cache all JS/CSS cache-first - Cache the Application Shell - Precache bundled loaded by the shell - Webpack runtime, vendor, entry - Named chunks to cache async routes

Slide 56

Slide 56 text

/* global $VERSION, $Cache, importScripts, WorkboxSW */ importScripts('https://unpkg.com/[email protected]/build/importScripts/workbox- sw.prod.v1.1.0.js'); // Add app shell to the webpack-generated precache list $Cache.precache.push({url: 'sw-shell.html', revision: $VERSION}); // Register precache list with Workbox const workbox = new WorkboxSW({handleFetch: true, skipWaiting: true, clientClaim: true}); workbox.precache($Cache.precache); // Runtime cache all js workbox.router.registerRoute(/webapp\/js\/.*\.js/, workbox.strategies.cacheFirst()); // Prefer app-shell for full-page loads workbox.router.registerNavigationRoute('sw-shell.html', { blacklist: [ // bunch of non-app routes
 ], }); SW Caching

Slide 57

Slide 57 text

Application Shell Pattern Caching Pinterest’s UI assets for instant repeat loads - Tricky: historical initial payloads contained user experiments, user info, contextual info. Cache? Take a perf hit of render blocking request before rendering anything? - Decided to cache this in the AppShell. Managed when to “invalidate” the shell - Each request has an app version. If the version changes, unregister the SW, register the new one. - Experiment groups cache in localStorage and update manually.

Slide 58

Slide 58 text

Future - Fixing slow API responses (home-feed takes 1s on Fast 3G) - Optimize server latency and response sizes - Adding for preloading bundles

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

Before

Slide 61

Slide 61 text

After

Slide 62

Slide 62 text

Performance Budgets Budgets Tinder tries not to exceed - Main/vendor chunk: 155KB - Async common chunks: 55KB - Other chunks: 35KB - CSS hard limit of 20KB

Slide 63

Slide 63 text

import A from '../A'; import B from '../B'; const route = [{ route: '/', regions: { side: A, main: B } }]; JavaScript Route-based code-splitting Before

Slide 64

Slide 64 text

import Loadable from 'react-loadable'; const A = Loadable({ loader: () => import('../A' /* webpackChunkName: "pc-r-A" */), loading: () => null }); const B = Loadable({ loader: () => import('../B' /* webpackChunkName: "pc-r-B" */), loading: () => null }); const route = [{ route: '/', regions: { side: A, main: B }, preload: [/* next page chunk to preload*/] }]; React Router React Loadable CommonsChunkPlugin After

Slide 65

Slide 65 text

const LoadableComponent = Loadable({…}); LoadableComponent.preload(); JavaScript Route-based code-splitting Preloading more page chunks with React Loadable

Slide 66

Slide 66 text

JavaScript Route-based code-splitting Before Main bundle size: 166kb DOMContentLoad: 5.46s load: 11.91s

Slide 67

Slide 67 text

JavaScript Route-based code-splitting After Main bundle size: 101kb DOMContentLoad: 4.69s load: 4.69s

Slide 68

Slide 68 text

Improve JavaScript caching Trim down vendor/library invalidation Use a whitelist of external deps & split out Webpack manifest from main chunk. Bundle now ~160KB for both chunks. module.exports = [ 'immutable', 'lodash', 'lodash-es', 'react', 'react-dom', 'react-redux', 'react-router', 'redux', 'redux-persist', 'redux-immutable', 'redux-saga', ];

Slide 69

Slide 69 text

before after

Slide 70

Slide 70 text

new BundleAnalyzerPlugin({ analyzerMode: 'server', analyzerPort: 8888, reportFilename: 'report.html', openAnalyzer: true, generateStatsFile: false, statsFilename: 'stats.json', statsOptions: null }) Webpack Bundle Analysis

Slide 71

Slide 71 text

new LodashModuleReplacementPlugin({ caching: true, collections: true, paths: true, // replace find/findIndex later // https://github.com/lodash/lodash-webpack-plugin/issues/79 shorthands: true })

Slide 72

Slide 72 text

Bundle Analysis: Login Page

Slide 73

Slide 73 text

CSS Loading Strategy Atomic CSS - Using Atomic CSS to create highly reusable CSS styles - All atomic CSS style are inlined in the initial paint - Some CSS loaded in stylesheet (including animations) before after

Slide 74

Slide 74 text

Runtime Performance Beacons and Swipes - requestIdleCallback() defer non-critical actions into idle - Simplified HTML composite layers to reduce paint counts

Slide 75

Slide 75 text

new webpack.optimize.ModuleConcatenationPlugin(), new webpack.DefinePlugin({ 'process.env': { // Env stuff... }), Update to Webpack 3 JavaScript Parsing Time: 250ms -> 230ms (8%) React 15.x -> React 16 reduced vendor chunks by ~6.7%

Slide 76

Slide 76 text

new workboxWebpackPlugin({ injectManifest: true, swSrc: paths.serviceWorkerSrcPath, swDest: paths.serviceWorkerDestPath, globDirectory: paths.staticPath, globPatterns: [ '**/*.{js,html,css,svg,woff2}' ], templatedUrls: { 'index.html': [ '../public/static/build/main-*.js', '../public/static/build/vendor-*.js', '../public/static/build/manifest-*.js', '../public/static/build/style.*.css'] }, maximumFileSizeToCacheInBytes: 4194304 })

Slide 77

Slide 77 text

Resources

Slide 78

Slide 78 text

• Fast By Default: Modern Loading Practices (https://goo.gl/CnxikT) • JavaScript Startup Performance (https://goo.gl/HHzUSh) • The Cost-of JavaScript (https://goo.gl/yv7j2j) • Pinterest PWA Case Study (https://goo.gl/PVBjFW) • Tinder PWA Case Study (https://goo.gl/vrtjFi) • Chrome User Experience Report (https://goo.gl/pJpYUx) • User Centric Performance Metrics (https://goo.gl/kKRduJ) • Can You Afford It?: Real-world Web Performance Budgets (https:// goo.gl/WyGNs3)

Slide 79

Slide 79 text

Thank you @bcinarli