Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fast by Default: Modern Loading Practices

Fast by Default: Modern Loading Practices

This presentation is a modified version of Addy Osmani's "Fast By Default" presentation from Chrome Dev Summit 2017 (https://www.youtube.com/watch?v=_srJ7eHS3IM)

Bilal Çınarlı

June 06, 2018
Tweet

More Decks by Bilal Çınarlı

Other Decks in Technology

Transcript

  1. 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 *.*
  2. Improving your performance and applying best practices is a journey

    where small changes can lead to significant achievements
  3. WebPack is a module bundler which has lots of perks.

    Code Splitting, Chunking, Tree-shaking…
  4. // Network type that browser uses navigator.connection.type > ‘wifi’ //

    New: effective connection type // using rtt and downlink values navigator.connection.effectiveType > ‘2G’
  5. <link rel="preload" href="movies.json" as="fetch"> <script> (async () => { try

    { const response = await fetch("movies.json"); const data = await response.json(); console.log(data); } catch (e) { console.log("Booo"); } })(); </script>
  6. addEventListener('activate', event => { event.waitUntil(async function() { // Feature-detect if

    (self.registration.navigationPreload) { // Enable navigation preloads! await self.registration.navigationPreload.enable(); } }()); });
  7. 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
  8. 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
  9. Old Mobile Site - 1st load First Paint: 4.2s First

    Meaningful Paint: 6.2s Time To Consistently Interactive: 23s
  10. New Mobile Site - 1st load First Paint: 1.8s First

    Meaningful Paint: 5.1s Time To Consistently Interactive: 5.6s
  11. New Mobile Site - Repeat Loads First Paint: 0.6s First

    Meaningful Paint: 3.5s Time To Consistently Interactive: 3.9s
  12. Performance 20s ➡ 6.5s P90 for Pin pages 13s ➡

    4s P50 for Pin pages 620KB ➡ 150KB JS Bundles 150KB ➡ 6KB inline CSS Bundles
  13. 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)
  14. 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' };
  15. // 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 <Route exact key="matched-route" path={path} render={ matchProps => <PageRoute bundleLoader={loader} routeName={name} {...matchProps} {...props} /> }/> React Router V4
  16. // Async load the route bundle class PageRoute extends PureComponent

    { render() { const {bundleLoader, ...props} = this.props; return <Loader loader={bundleLoader} {...props} />; } } // Load it and render class Loader extends PureComponent { componentWillMount() { this.props.loader().then(module => { this.setState({LoadedComponent: module.default}); }); } } React Router V4
  17. 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
  18. 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
  19. /* 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
  20. 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.
  21. Future - Fixing slow API responses (home-feed takes 1s on

    Fast 3G) - Optimize server latency and response sizes - Adding <link rel=preload as=script> for preloading bundles
  22. Performance Budgets Budgets Tinder tries not to exceed - Main/vendor

    chunk: 155KB - Async common chunks: 55KB - Other chunks: 35KB - CSS hard limit of 20KB
  23. import A from '../A'; import B from '../B'; const route

    = [{ route: '/', regions: { side: A, main: B } }]; JavaScript Route-based code-splitting Before
  24. 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
  25. 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', ];
  26. new BundleAnalyzerPlugin({ analyzerMode: 'server', analyzerPort: 8888, reportFilename: 'report.html', openAnalyzer: true,

    generateStatsFile: false, statsFilename: 'stats.json', statsOptions: null }) Webpack Bundle Analysis
  27. new LodashModuleReplacementPlugin({ caching: true, collections: true, paths: true, // replace

    find/findIndex later // https://github.com/lodash/lodash-webpack-plugin/issues/79 shorthands: true })
  28. 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
  29. Runtime Performance Beacons and Swipes - requestIdleCallback() defer non-critical actions

    into idle - Simplified HTML composite layers to reduce paint counts
  30. 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%
  31. 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 })
  32. • 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)