thank you very much
dziękuję bardzo
#performance⚡️ #react ⚛️
@midudev
What frontend library
do you use?
Text
@midudev
📷 🏠
@midudev
february 2019
Speed Index
7s
Performance Score
Time to Interactive
15s
🏋️ x 4
🏋️ x 5
@midudev
@midudev
TTI
15 seconds
📹
@midudev
❤️
@midudev
Is slow?
We asked ourselves...
@midudev
february 2019
Speed Index
7s
Performance Score
Time to Interactive
15s
@midudev
today
Speed Index
3.9s
Performance Score
Time to Interactive
7s
@midudev
Our problem is not React anymore
@midudev
@midudev
How do we have achieved this?
-2s
-1.5s
@midudev
@midudev
@midudev
@midudev
Time to Interactive
Other performance metrics
Static files
👀 hydration could be needed
export
Inflexible
✅
❌
@midudev
Slow Time to First Byte
Maintain different codebases
✅
Time To Interactive
First Contentful Paint
SEO Friendly
❌
@midudev
Flexible
Fast TTFB and FCP!
Just static files
TTI way after FCP
SEO concerns
✅
❌
@midudev
@midudev
hydration data
html from server
}
@midudev
const initialData = await getInitialData()
const html = ReactDOM.renderToString(
<App initialData={initialData} />
)
res.send(`
<div id='root'>${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(initialData)}
</script>`
)
ReactDOM.hydrate(
<App initialData={window.__INITIAL_DATA__} />,
document.getElementById('root')
)
// SERVER SIDE simplified
// CLIENT SIDE simplified
🏁 Attach event listeners to the existing markup
🔎 Check mismatches between server-side and client-side
🌳 Recreate tree on the virtual dom
🚀 Set up life cycles order and fire them
⚛️ constructor, componentDidMount, useEffect, ...
ReactDOM.hydrate
app.$mount('#root')
@midudev
}
⏳
ReactDOM.hydrate
from server
client after hydration
@midudev
@midudev
Universal code
Flexible
SEO Friendly
Faster Speed Index
@midudev
Slow Time To First Byte from SSR
Bad Time to Interactive from CSR
@midudev
@midudev
6x
1.8s
43KB
🗜️
@midudev
@midudev
@midudev
@midudev
@midudev
@midudev
@midudev
by route
@midudev
// 🚇 middleware to activate Dynamic Rendering on Express
const BOTS_USER_AGENTS = [
'googlebot',
'mediapartners-google',
'yandexbot'
]
const INDEX_HTML_PATH = path.join(__dirname, 'public', 'index.html')
const HTML_TEMPLATE = fs.readFileSync(INDEX_HTML_PATH, 'utf8')
module.exports = function middleware(req, res, next) {
const rawUserAgent = req.get('user-agent')
const userAgent = rawUserAgent.toLowerCase()
// check if the request comes from a bot 🤖
if (BOTS_USER_AGENTS.find(ua => userAgent.includes(ua))) {
// if not an user, just use the normal behaviour (SSR?)
return next()
}
// return index.html directly if is an user 👩💻
return res.send(HTML_TEMPLATE)
}
// 🚆 Using the middleware in Express
const dynamicRendering = require('./dynamic-rendering')
// Use dynamic rendering for a specific path for testing
app.get(
'/es/:op/:type/barcelona-capital/*',
dynamicRenderingExperiment
)
// Isomorphic routes handler
app.get('*', serverSideRenderingMiddleware)
next()
response html
👩💻
🤖
@midudev
by route
♻️ Fast TTFB
🧹 Hydration data is gone
1️⃣ Only changes on the server side
🤖 SEO Friendly
🧪 Useful technique for perf experiments
🆓 Free resources from your server
🤔 Just a Client Side Rendering for the user
@midudev
at component level
@midudev
@midudev
at component level
@midudev
⚛️🌲
🤖 🌍
💁♂️ 🌍
👀
👀
viewport
@midudev
@midudev
<DynamicRendering userAgent={universalUserAgent}>
<a href='https://very-interesting-url.com'>
<VeryComplexToComputeComponent />
<img src='https://huge-image.com/panda.jpg' />
</a>
</DynamicRendering>
@schibstedspain/react-perf-dynamic-rendering
http://bit.ly/sui-dynamic-rendering
// ⚛️ <DynamicRendering /> ~"kinda" implementation
export default function DynamicRendering({ children, userAgent }) {
const isBot = checkUserAgentIsBot({userAgent})
if (isBot) return children
if (typeof window !== 'undefined') { // isBrowser
return <LazyContent>{children}</LazyContent>
} else { // isServer
return <div style={{border: '1px solid red'}} />
}
}
@midudev
📏 ~700px visible
>20000px rendered 👉
😱
👀 only 1 card
34 cards
1x
@midudev
@midudev
6x
~350ms
23KB
🗜️
-1.5s
-20KB
at component level
✅ Improve TTI
😴 Lazy Load for the user
🏋️♀️ Perfect for stuff below the fold
⏳ Help GoogleBot to index your content faster
🚩 Keep Hydration data
👩🔬 Need universal UA calculation
🤖 Bot still gets full cost SSR with hydration
@midudev
@midudev
@midudev
links detected
at a later time
💸 expensive!
https://youtu.be/LXF8bM4g-J4
WRS
@midudev
@midudev
@midudev
@midudev
at component level
@midudev
🏁 Attach event listeners to the existing markup
🔎 Check mismatches between server-side and client-side
🌳 Recreate tree on the virtual dom
🚀 Set up life cycles order and fire them
constructor, componentDidMount, useEffect, ...
@midudev
@midudev
// ⚛️ <StaticContent /> usage
import StaticContent from './StaticContent'
function Footer () {
return (
<StaticContent>
<HugeListOfLinks data={listOfLinks} />
</StaticContent>
)
}
// ⚛️ <StaticContent /> ~implementation
import React from 'react'
export default function StaticContent({children}) {
// we're in the server, just render the content
if (typeof window === 'undefined') {
return <div>{children}</div>
}
// avoid re-render on the client
return (
<div
dangerouslySetInnerHTML={{__html: ''}}
suppressHydrationWarning
/>
)
}
⚛️ core contributor
dangerouslySetInnerHTML
about
@midudev
suppressHydrationWarning
about
@midudev
@midudev
6x
~235ms
44KB
🗜️
-1.6s
+1KB
@midudev
at component level
⚡ Avoid re-hydrate for static components
🤳 Thus could greatly improve TTI
📸 For expensive rendering lists or static content (SEO Footers)
🤖 GoogleBot is definitely going to detect it
⚠️ Lose interactivity!!!!!
🏋️♀️ Hydration data still there
🥪 Element wrapper (ex. <div>)
@midudev
@midudev
@midudev
at component level
➕
@midudev
@midudev
👀
👀
👀
@midudev
@schibstedspain/react-perf-dynamic-rendering
http://bit.ly/sui-dynamic-rendering
// ⚛️ How to use <ProgressiveRendering />
function ProgressiveHydrationUsage({articles}) {
return (
<Grid>
<h1>Articles</h1>
{articles.map(article => (
<ProgressiveHydration key={article.id}>
<Card {...article} />
</ProgressiveHydration>
))}
</Grid>
)
}
@schibstedspain/react-perf-dynamic-rendering
http://bit.ly/sui-dynamic-rendering
// ⚛️ <ProgressiveRendering /> ~"kinda" implementation
function ProgressiveHydration({children}) {
const ref = useRef(null)
const isNearScreen = useNearScreen({ref})
useEffect(() => {
// CLIENT: If the element is near screen, then hydrate it
if (isNearScreen) {
ReactDOM.hydrate(children, ref.current)
}
}, [children, isNearScreen])
// SERVER: Just render the content as usual
if (typeof window === 'undefined')
return <div ref={ref}>{children}</div>
// CLIENT: Avoid hydration until we say so
return (
<div ref={ref}
suppressHydrationWarning
dangerouslySetInnerHTML={{__html: ''}}
/>
)
}
vue-lazy-hydration by maoberlehner
Lazy hydration of server-side rendered Vue.js components
lazy-hydration by znck
Lazy Hydration for Vue SSR
Simple API. Plug and play.
Configurable interaction to fire hydration.
@midudev
<template>
<div class="ArticlePage">
<LazyHydrate when-idle>
<ImageSlider/>
</LazyHydrate>
<LazyHydrate ssr-only>
<ArticleContent :content="article.content"/>
</LazyHydrate>
<LazyHydrate when-visible>
<AdSlider/>
</LazyHydrate>
<!-- `on-interaction` listens for a `focus` by default ... -->
<LazyHydrate on-interaction>
<CommentForm :article-id="article.id"/>
</LazyHydrate>
<!-- ... but you can listen for any event you want ... -->
<LazyHydrate on-interaction="click">
<CommentForm :article-id="article.id"/>
</LazyHydrate>
<!-- ... or even multiple events. -->
<LazyHydrate :on-interaction="['click', 'touchstart']">
<CommentForm :article-id="article.id"/>
</LazyHydrate>
</div>
</template>
vue-lazy-hydration
@midudev
react-prerendered-component by theKashey
Partial Hydration and Component Level Caching
Powerful 💪 but complicated 🤹♂️
react-progressive-hydration by
Progressive Hydration
Experimental 🧪 but interesting 🤯
react-lazy-hydration by hadeeb
Not hydrating but rendering later 🎨. Based on vue lazy-hydration.
@midudev
@midudev
6x
~235ms
44KB
🗜️
-1.6s
+1KB
@midudev
~30ms
on demand
👀 Only hydrate what's visible
🤳 Thus could greatly improve TTI
🔛 Activate interactivity on demand
📸 Rich lazy loading experience
🤖 GoogleBot will get the rendered static html (not hydrated)
🏋️♀️ Hydration data still there
🥪 Element wrapper (ex. <div>)
⚠️ No Context passed down!!!!
@midudev
@midudev
from Andrew Clarke presentation: https://youtu.be/z-6JC0_cOns?t=950
Can't start rendering anything until everything is loaded
Encapsulation is difficult (no state or lifecycles)
Need to hoist all async data
@midudev
@midudev
@midudev
@midudev
ReactDOMServer.renderToNodeStream(
<App initialData={initialData} />
).pipe(res)
@midudev
@midudev
How have we achieved this?
@midudev
could be slow.
Now you have
strategies to fix it.
@midudev
@midudev/react-dynamic-rendering
@midudev/react-static-content
@midudev/react-progressive-hydration
@midudev
Requirements:
isomorphic React ⚛️
and your custom solution
Compatibility:
Bundle size:
@midudev
rendering strategies
@midudev
@midudev
thank you very much
dziękuję bardzo