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

More Decks by Bilal Çınarlı

Other Decks in Technology


  1. Fast By Default Modern Loading Best Practices

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

  3. None
  4. First Paint First Meaningful Paint Time To Interactive USER-HAPPINESS

  5. How to be fast? By following best practices also, investing

    in your code!
  6. 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 *.*
  7. Improving your performance and applying best practices is a journey

    where small changes can lead to significant achievements
  8. Being Progressive

  9. None
  10. You can load instantly in any network, even pre-caching resources

    to reduce the need for the network!
  11. None
  12. Become ridiculously fast! Once loaded, users expect pages to be


  14. Where do mobile sites spend their time loading? With thanks

    to Camillo and Mathias @ V8
  15. 2017 JavaScript Parse Costs Average Phone ~1MB JavaScript (uncompressed)

  16. JavaScript Parse Cost On Mobile - CNN ~9s difference to

    the A11
  17. Follow Patterns

  18. PRPL Pattern INSPIRED

  19. Use Your Budget Wisely

  20. None
  21. None
  22. https://twitter.com/kristoferbaxter/status/908144931125858304

  23. None
  24. None
  25. Change Your Coding Style

  26. WebPack is a module bundler which has lots of perks.

    Code Splitting, Chunking, Tree-shaking…
  27. None
  28. Create a Lab Environment

  29. Chrome DevTools Lighthouse WebPageTest

  30. Use New Techniques Now!

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

  32. @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; }
  33. I want to adapt serving based on network quality

  34. // Network type that browser uses navigator.connection.type > ‘wifi’ //

    New: effective connection type // using rtt and downlink values navigator.connection.effectiveType > ‘2G’
  35. I have a critical resource, I want to load earlier

    than the discovery
  36. <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>
  37. I want to start network requests while Service Worker is

    still booting up
  38. addEventListener('activate', event => { event.waitUntil(async function() { // Feature-detect if

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

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

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

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

    4s P50 for Pin pages 620KB ➡ 150KB JS Bundles 150KB ➡ 6KB inline CSS Bundles
  47. 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)
  48. 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' };
  49. // 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
  50. // 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
  51. Webpack Bundle Analysis Using webpack-bundle-analyzer

  52. 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
  53. Before splitting out common async route code

  54. After moving out common code from async chunks into entryChunk:

  55. 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
  56. /* global $VERSION, $Cache, importScripts, WorkboxSW */ importScripts('https://unpkg.com/workbox-sw@1.1.0/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
  57. 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.
  58. 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
  59. None
  60. Before

  61. After

  62. Performance Budgets Budgets Tinder tries not to exceed - Main/vendor

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

    = [{ route: '/', regions: { side: A, main: B } }]; JavaScript Route-based code-splitting Before
  64. 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
  65. const LoadableComponent = Loadable({…}); LoadableComponent.preload(); JavaScript Route-based code-splitting Preloading more

    page chunks with React Loadable
  66. JavaScript Route-based code-splitting Before Main bundle size: 166kb DOMContentLoad: 5.46s

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

    load: 4.69s
  68. 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', ];
  69. <link rel=preload as=script> before <link rel=preload as=script> after

  70. new BundleAnalyzerPlugin({ analyzerMode: 'server', analyzerPort: 8888, reportFilename: 'report.html', openAnalyzer: true,

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

    find/findIndex later // https://github.com/lodash/lodash-webpack-plugin/issues/79 shorthands: true })
  72. Bundle Analysis: Login Page

  73. 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
  74. Runtime Performance Beacons and Swipes - requestIdleCallback() defer non-critical actions

    into idle - Simplified HTML composite layers to reduce paint counts
  75. 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%
  76. 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 })
  77. Resources

  78. • 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)
  79. Thank you @bcinarli