Increase the Performance of your Site with Lazy-Loading and Code-Splitting

A presentation at Speeding things up in React - Stockholm ReactJS Meetup in September 2018 in Stockholm, Sweden by José M. Pérez

Slide 1

Slide 1

Increase the Performance of your Site with Lazy‑Loading and Code‑ Splitting

Slide 2

Slide 2

About me I'm Jose and I work as Engineering Manager in Spotify I like sites (building/using) with good performance @jmperezperez on Twitter

Slide 3

Slide 3

The era of components

Slide 4

Slide 4

What we'll talk about

Slide 5

Slide 5

Compositional Patterns

Slide 6

Slide 6

High Order Components const MyComponent = props => ( <div> {props.id} - {props.name} </div> ); // ... const ConnectedComponent = connect( mapStateToProps, mapDispatchToProps )(MyComponent);

Slide 7

Slide 7

Function as Child Component aka Render Callback const MyComponent = () => ( <Media query="(max-width: 599px)"> {matches => matches ? ( <p>The document is less than 600px wide.</p> ) : ( <p>The document is at least 600px wide.</p> ) } </Media> );

Slide 8

Slide 8

Improving performance of our sites by loading only what is needed

Slide 9

Slide 9

Slide 10

Slide 10

Most times you would include all the scripts and CSS needed to render all sections as soon as the user visits the page. Until recently it was difficult to define a module’s dependencies, and load what was needed.

Slide 11

Slide 11

How likely is it for the user to see the header? What about the map?

Slide 12

Slide 12

Yahoo's YUI Loader Facebook's Haste, Bootloader and Primer

Slide 13

Slide 13

Lazy‑Loading has trade‑offs too

Slide 14

Slide 14

Slide 15

Slide 15

Slide 16

Slide 16

Invisible content in some scenarios printing the page RSS readers SEO

Slide 17

Slide 17

A small component to detect when an area is visible

Slide 18

Slide 18

Function as Child Component aka Render Callback class Observer extends Component { constructor() { super(); this.state = { isVisible: false }; this.io = null; this.container = null; } componentDidMount() { this.io = new IntersectionObserver([entry] => { this.setState({ isVisible: entry.isIntersecting }); }, {}); this.io.observe(this.container); } componentWillUnmount() { if (this.io) { this.io.disconnect(); } } render() { return ( // we create a div to get a reference. // It's possible to use findDOMNode() to avoid

Slide 19

Slide 19

Using it const Page = () => { <div> <Header /> <Observer>{isVisible => <Gallery isVisible />}</Observer> <Observer>{isVisible => <Map isVisible />}</Observer> <Footer /> </div>; };

Slide 20

Slide 20

make sure that you reserve the area for the lazy‑loaded component

Slide 21

Slide 21

class Map extends Component { constructor() { super(); this.state = { initialized: false }; this.map = null; } initializeMap() { this.setState({ initialized: true }); // loadScript loads an external script, its definition is not included here. loadScript('https://maps.google.com/maps/api/js?key=<your_key>', () => { const latlng = new google.maps.LatLng(38.34, -0.48); const myOptions = { zoom: 15, center: latlng }; const map = new google.maps.Map(this.map, myOptions); }); } componentDidMount() { if (this.props.isVisible) { this.initializeMap(); } } componentWillReceiveProps(nextProps) { if (!this state initialized && nextProps isVisible) {

Slide 22

Slide 22

class Gallery extends Component { constructor() { super(); this.state = { hasBeenVisible: false }; } componentDidMount() { if (this.props.isVisible) { this.setState({ hasBeenVisible: true }); } } componentWillReceiveProps(nextProps) { if (!this.state.hasBeenVisible && nextProps.isVisible) { this.setState({ hasBeenVisible: true }); } } render() { return ( <div> <h1>Some pictures</h1> Picture 1 {this.state.hasBeenVisible ? ( <img src="http://example.com/image01.jpg" width="300" height="300" /> ) : (

Slide 23

Slide 23

Stateless Child Components

Slide 24

Slide 24

const Gallery = ({ isVisible }) => ( <div> <h1>Some pictures</h1> Picture 1 {isVisible ? ( <img src="http://example.com/image01.jpg" width="300" height="300" /> ) : ( <div className="placeholder" /> )} Picture 2 {isVisible ? ( <img src="http://example.com/image02.jpg" width="300" height="300" /> ) : ( <div className="placeholder" /> )} </div>

Slide 25

Slide 25

const Page = () => { ... <Observer> {(isVisible, hasBeenVisible) => <Gallery hasBeenVisible /> // Gallery can be now stateless } </Observer> ... }

Slide 26

Slide 26

constructor() { super(); this.state = { hasBeenVisible: false }; this.io = null; this.container = null; } componentDidMount() { this.io = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { this.setState({ hasBeenVisible: true }); this.io.disconnect(); } }); }, {}); this.io.observe(this.container); } componentWillUnmount() { if (this.io) { this.io.disconnect(); } } render() { return ( <div ref={div => { this.container = div; }} > {Array.isArray(this.props.children) ? this.props.children.map(child => child(this.state.hasBeenVisible)) : this.props.children(this.state.hasBeenVisible)} </div> );

Slide 27

Slide 27

More use cases

Slide 28

Slide 28

super(); this.state = { progress: 0 }; this.interval = null; this.animationDuration = 2000; this.startAnimation = null; } componentWillReceiveProps(nextProps) { if ( !this.props.isVisible && nextProps.isVisible && this.state.progress !== 1 ) { this.startAnimation = Date.now(); const tick = () => { const progress = Math.min( 1, (Date.now() - this.startAnimation) / this.animationDuration ); this.setState({ progress: progress }); if (progress < 1) { requestAnimationFrame(tick); } }; tick(); } } render() { return ( <div> {Math.floor(this.state.progress * 3)} days · {Math.floor(this.state.progress * 21)} talks · {Math.floor(this.state.progress * 4)} workshops · {Math.floor(this.state.progress * 350)} attendees </div>

Slide 29

Slide 29

Polyfilling IntersectionObserver on‑demand

Slide 30

Slide 30

Disabling lazy‑loading if IntersectionObserver is not supported class Observer extends Component { constructor() { super(); // isVisible is initialized to true if the browser // does not support IntersectionObserver API this.state = { isVisible: !(window.IntersectionObserver) }; this.io = null; this.container = null; } componentDidMount() { // only initialize the IntersectionObserver if supported if (window.IntersectionObserver) { this.io = new IntersectionObserver(entries => { ... } } } }

Slide 31

Slide 31

Requesting a polyfill on demand class Observer extends Component { ... componentDidMount() { (window.IntersectionObserver ? Promise.resolve() : import('intersection-observer') ).then(() => { this.io = new window.IntersectionObserver(entries => { entries.forEach(entry => { this.setState({ isVisible: entry.isIntersecting }); }); }, {}); this.io.observe(this.container); }); } ... }

Slide 32

Slide 32

Safari requests the polyfill for intersection‑observer on demand. No need to ship it to browsers that support it natively.

Slide 33

Slide 33

Code Splitting and CSS‑in‑JS react‑router and Next.js have made code‑splitting easy to implement lazy‑loading can be applied to other resources (SVGs, CSS) With CSS‑in‑JS we take code splitting further, loading CSS on demand.

Slide 34

Slide 34

Useful implementations thebuilder/react‑intersection‑observer researchgate/react‑intersection‑observer

Slide 35

Slide 35

Conclusion Componentization makes code‑splitting and loading assets on‑demand easier than ever!