feat: Optimize StaticHtml component for React js 🚀 #14917
Conversation
Refactors the StaticHtml component to use React.memo for efficient static HTML rendering, improving performance and compatibility.
🦋 Changeset detectedLatest commit: 081e010 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
florian-lefebvre
left a comment
There was a problem hiding this comment.
I don't feel confident approving this given how sensitive it is (react is one of our most uses integrations), I'll wait for someone else. Anyways, can you add a patch changeset? Thank you!
| StaticHtml.shouldComponentUpdate = () => false; | ||
|
|
||
| export default StaticHtml; | ||
| export default memo(StaticHtml, () => true); |
There was a problem hiding this comment.
Just noting that memo is indeed noted as the equivalent at the end of https://react.dev/reference/react/Component#shouldcomponentupdate. Memo docs: https://react.dev/reference/react/memo
There was a problem hiding this comment.
Just noting that
memois indeed noted as the equivalent at the end of https://react.dev/reference/react/Component#shouldcomponentupdate. Memo docs: https://react.dev/reference/react/memo
Thanks for checking! You are correct that they serve the same purpose. But actually, shouldComponentUpdate is only for Class components, so it gets ignored here. The old code wasn't really running. Switching to memo ensures it works properly.
Sorry if I am wrong
There was a problem hiding this comment.
I wouldn't be surprised if it worked, undocumented
There was a problem hiding this comment.
I wouldn't be surprised if it worked, undocumented
It appears the previous implementation was ineffective and had no measurable impact. Here is the evidence:
View Benchmark Code
import React, { useState } from 'react';
import TestRenderer from 'react-test-renderer';
import { performance } from 'perf_hooks';
// --- 1. Setup: Render Counters ---
let oldRenderCount = 0;
let newRenderCount = 0;
// --- 2. Components ---
const OldStaticHtml = ({ value, name }) => {
oldRenderCount++; // Track how many times React actually runs this function
return React.createElement('astro-slot', { name, dangerouslySetInnerHTML: { __html: value } });
};
OldStaticHtml.shouldComponentUpdate = () => false;
const NewStaticHtml = React.memo(({ value, name }) => {
newRenderCount++; // Track how many times React actually runs this function
return React.createElement('astro-slot', { name, dangerouslySetInnerHTML: { __html: value } });
});
// --- 3. The "Real World" Parent ---
// This component simulates a page that re-renders frequently (e.g., a timer)
function App({ ComponentToTest, value }) {
return React.createElement(
'div',
{ className: 'page' },
React.createElement('h1', null, 'App Header'),
React.createElement(ComponentToTest, { value, name: 'content' }),
);
}
// --- 4. The Benchmark Runner ---
function runBenchmark(label, ComponentToTest, iterations = 10000) {
const htmlContent = '<div>Static Content</div>';
const root = TestRenderer.create(
React.createElement(App, { ComponentToTest, value: htmlContent }),
);
// Reset counters
oldRenderCount = 0;
newRenderCount = 0;
const start = performance.now();
// Simulate 1000 parent re-renders
for (let i = 0; i < iterations; i++) {
root.update(React.createElement(App, { ComponentToTest, value: htmlContent }));
}
const end = performance.now();
const count = label.includes('Old') ? oldRenderCount : newRenderCount;
console.log(`${label}:`);
console.log(` - Time: ${(end - start).toFixed(2)}ms`);
console.log(` - Actual Render Calls: ${count}`);
console.log('-----------------------------------');
}
console.log('🚀 Running Real-World Simulation (10000 Parent Updates)...\n');
runBenchmark('❌ Old (No Memo)', OldStaticHtml);
runBenchmark('✅ New (Memo)', NewStaticHtml);There was a problem hiding this comment.
I ran some benchmarks to test the impact of memoization on components with heavy computations (simulating real-world scenarios like expensive data processing or complex logic).
The results show that in complex cases, the performance gain is massive—nearly 205x faster compared to unmemoized components 🚀
Click to see the Benchmark Code
// @vitest-environment jsdom
import { bench, describe } from 'vitest';
import React, { memo } from 'react';
import { render } from '@testing-library/react';
// Simulates heavy work (e.g., complex integration/data processing)
function findNthPrime(n: number): number {
let count = 0;
let num = 2;
while (count < n) {
let isPrime = true;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
num++;
}
return num - 1;
}
const LIST_SIZE = 300;
const STATIC_ITEMS = Array.from({ length: LIST_SIZE / 2 }, (_, i) => `Static-${i}`);
const DYNAMIC_ITEMS = Array.from({ length: LIST_SIZE / 2 }, (_, i) => i);
const DynamicItem = ({ index, searchText }: { index: number; searchText: string }) => {
const complexValue = findNthPrime(300 + (index % 50)).toExponential();
return (
<div>
<strong>Item {index}:</strong> {searchText}
<small>(Calculated: {complexValue})</small>
</div>
);
};
const ListComponent = ({ searchText }: { searchText: string }) => (
<div>
{STATIC_ITEMS.map((item) => <div key={item}>Fixed: {item}</div>)}
{DYNAMIC_ITEMS.map((i) => <DynamicItem key={i} index={i} searchText={searchText} />)}
</div>
);
const MemoizedListComponent = memo(ListComponent, () => true);
const App = ({ text, memoEnabled }: { text: string; memoEnabled: boolean }) => (
<div>
{memoEnabled ? <MemoizedListComponent searchText={text} /> : <ListComponent searchText={text} />}
</div>
);
const { rerender: rerenderNormal } = render(<App text="init" memoEnabled={false} />);
const { rerender: rerenderMemo } = render(<App text="init" memoEnabled={true} />);
describe('Complex React Performance', () => {
bench('Normal (Re-calculates on every type)', () => {
rerenderNormal(<App text={Math.random().toString()} memoEnabled={false} />);
});
bench('Memoized (Skips Calculation)', () => {
rerenderMemo(<App text={Math.random().toString()} memoEnabled={true} />);
});
});This confirms that while simple renders might not show a huge difference, when heavy work is involved, preventing unnecessary re-renders is critical. This is exactly why deep dependency tracking (like what the React Compiler aims to do) is so valuable for complex UIs.
Anyway, I wanted to share these findings—sorry if I'm mistaken, but I believe this demonstrates the real-world value of these optimizations!
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
|
Hi @florian-lefebvre, sorry to bother you again. Could you please let me know if this PR looks good now ? |
sarah11918
left a comment
There was a problem hiding this comment.
Thanks for making Astro better, @sanjaiyan-dev ! 🚀 Quick note from docs on the changeset!
More detailed changeset. Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
Thanks 🙏 |
|
Hi @florian-lefebvre , I tested two scenarios:
📊 Benchmark Results
(Benchmarks run on Node.js with 💡 Key TakeawaysThe 62% improvement on the smaller dataset is the real winner here. In real-world usage (like a search bar or a toggle next to heavy static content), this change significantly reduces the "cost per frame," helping to prevent input lag. By switching to 🛠️ Click to view the benchmark code (benchmark.mjs)import React from 'react';
import TestRenderer from 'react-test-renderer';
import { performance } from 'perf_hooks';
// 1. Old Implementation (Buggy, Pre-PR #14917)
const OldStaticHtml = ({ value, name }) => {
if (!value) return null;
return React.createElement('astro-slot', {
name,
dangerouslySetInnerHTML: { __html: value },
});
};
OldStaticHtml.shouldComponentUpdate = () => false; // Ignored by React!
// 2. New Implementation (Optimized via PR #14917)
const NewStaticHtml = React.memo(({ value, name }) => {
if (!value) return null;
return React.createElement('astro-slot', {
name,
dangerouslySetInnerHTML: { __html: value },
});
}, () => true); // Forces React to skip re-renders
// 3. Parent Component to simulate updates
function App({ count, ComponentToTest }) {
// Simulate a massive block of static HTML generated by Astro
const heavyHtml = "<div class='heavy'>".repeat(100) + "Astro Content" + "</div>".repeat(100);
return React.createElement('div', null,
React.createElement('h1', null, `Render Count: ${count}`),
React.createElement(ComponentToTest, { value: heavyHtml, name: "default" })
);
}
// 4. Benchmark Runner
function runBenchmark(name, ComponentToTest, iterations = 100_000) {
// Initial render
const root = TestRenderer.create(React.createElement(App, { count: 0, ComponentToTest }));
const start = performance.now();
// Trigger continuous re-renders (simulating rapid state changes)
for (let i = 1; i <= iterations; i++) {
root.update(React.createElement(App, { count: i, ComponentToTest }));
}
const end = performance.now();
console.log(`${name}: ${(end - start).toFixed(2)}ms for ${iterations} updates`);
}
console.log('🚀 Benchmarking StaticHtml React Integration...\n');
runBenchmark('❌ Old StaticHtml (Unoptimized)', OldStaticHtml);
runBenchmark('✅ New StaticHtml (Optimized)', NewStaticHtml); |
|
Hi @florian-lefebvre and @matthewp 👋 I totally understand the need to be cautious here since React hydration is so sensitive. Are there any specific edge cases (like nested islands or dynamic slots) you're worried about? I'd be happy to write some extra tests for those to prove it's safe and help you feel confident moving this forward. Just let me know how I can help :) |
Princesseuh
left a comment
There was a problem hiding this comment.
Researched this a bit, asked AI etc and it seems fine.
Thanks :) |


This PR optimizes the StaticHtml component, addressing the limitations of the previous implementation (which relied on an incorrect
shouldComponentUpdatestrategy). It now correctly usesReact.memoto prevent unnecessary re-renders of the static HTML content. This approach is more performant and is the correct way to memoize function components.shouldComponentUpdateimplementation.shouldComponentUpdatewithReact.memousing custom comparison function to prevent re-renders.https://react.dev/reference/react/memo