SPA Performance
Intro
To minimize time to interactive, we begin by identifying critical resources to load first.
After picking all the ecosystem pieces, how much space are you left for your application code? Picking these pieces we consider:
Vendor libs, Entry (app shell, core logic, state store), Async route chunks (lazy loads).
Scenarios
Client Rendering Browser asks/gets HTML page, then asks/gets for CSS and JS. Parse and execute. Browser asks/gets JSON data from API server. ReactDOM.render can be called, which generates a DOM tree, which browsers layouts and paints. Cons: Steps taken in serial, not parallel.
Whole Page - Server Rendering From ReactEurope17 presentation Load data server side and (with React) use ReactDOMServer.renderToString() on the server to create the HTML. Transmit data with a script tag or via HTTP/2 Server Push.
Browser asks for HTML page. Server sends browser HEAD of the HTML document, and simultaneous requests data from API server. Browser requests CSS and JS, while data from API server is sent back to web server, which is sent back to Browser as JSON. Benefits: Parallel steps taken, a lot faster Cons: Needs a dynamic web server, More content overall, long time-period of non-interactive content with time proportional to content volume.
By itself, server rendering doesn't scale with more features (content blocks/widgets), because it'll add more CSS, JS, and work for server renderer. More content or features increases TTI Time to First Paint also increases as:
More server-rendered content increases Time to First Byte
More CSS at initial load increases Paint Time
Component - Server Rendering
HTML Streaming FTW
"The "renderToString" method is a synchronous single method on the server. But if we broke the page up into parts and sent it down? Use HTML Streaming and send parts of the page and it would render in pieces. Seems good no? Unfortunately sending only the HTML isn't good enough" and the dependencies (CSS, JS, JSON) need to coordinated loading. If the CSS and HTML are needed for painting, and JS and JSON for interactivity and data, then a logical load order would be:
CSS, HTML, JS, JSON -- for each section (Header, Content, Features)
Browser could then load, paint, and make interactive each component in desired order.
Note: Theoretically nice, but separate loading is difficult with many different possible approaches for each resource type (CSS, JS/JSON, HTML).
Process: Browser asks/gets HTML file from server, which requests data from API server, which sends server 1st component JSON. Web server calls renderToString on 1st component. API server can continue sending additional component data when available. When web server finishes rendering 1st component, it sends all (CSS, HTML, JS, JSON) for it to browser. Process continues for additional components.
Pros:
Fastest TTP, TTI
Added content/features doesn't affect TTI of previously loaded components
Fault tolerance for unresponsive components
Cons:
Splitting pages into components to load is difficult
Difficult to coordinate correct load order for individual components and their resources (CSS, HTML, JS, JSON). Solutions: Server push, HTML imports, other.
Minimum User Profile and Bundle Size
Establish a device and network connection profile, then consider the time to interactive budget. Then subtract the amount of time for DNS Lookup, TCP handshake, and https handshake. With the time left, calculate the compressed bundle size to deliver at the profiled user network speed.
Bundle Composition
There are multiple bundling strategies, with the most recommended being a combination of app shells and route-based bundles. Considering the initial baseline bundle and a TTI goal of 5 seconds, we should consider how large our framework needs to be, so that we have more space for application logic. Lightweight framework options include Preact, Vue, Svelte, and more.
Scope hoisting (Bundler) ?
Code Splitting Strategy
Vendor libs, Entry (app shell, core logic, state store), Async route chunks (lazy loads).
Bundler plugins
CommonsChunkPlugin,
Route-based Splitting
Using React Router, React Loadable, and CommonsChunkPlugin Preloading route scripts to cache so they'd be available to the user at a later point in time.
Network transfer times
Parsing / Compiling times
Runtime costs
Library sharding?
link rel="preload", can be used with Fetch API
Bundle Analysis
If some libraries are shared across multiple modules, it might make more sense to load them earlier if the subsequent chunk sizes can be dramatically reduced.
With ES module support increasing, there should be a point where they can be used without any build steps.
Tools: webpack bundle analyzer, source map explorer
From Chrome Dev Summit 2017, Tinder:
Core-js + babel-preset-env to drop unused polyfills and only transpile code for browsers that need it. Use lodash-webpack-plugin (or babel-plugin-lodash) to reduce bundle size Replaced localForage with IndexedDB Splot non-critical components not used for First Paint Removed critical CSS from bundle (SSRs already).
Last updated