Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c9037f5
feat: Optimize StaticHtml component for React js
sanjaiyan-dev Nov 27, 2025
a1a29c3
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Nov 29, 2025
2a7293a
Merge branch 'withastro:main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Dec 4, 2025
b39a4f9
Add Changset 🌟
sanjaiyan-dev Dec 4, 2025
cea0bba
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Dec 16, 2025
7306953
Merge branch 'main' into sanjaiyan-react-html-optmize
florian-lefebvre Feb 4, 2026
7e686c7
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Feb 4, 2026
c1ef2d6
Update .changeset/tough-days-sell.md
sanjaiyan-dev Feb 4, 2026
4562da9
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Feb 12, 2026
bc33f34
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Feb 12, 2026
0a083e9
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Feb 13, 2026
1022279
Update .changeset/tough-days-sell.md
sanjaiyan-dev Feb 16, 2026
97cb262
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Feb 16, 2026
d6f6da0
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Feb 24, 2026
2634fd0
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Mar 7, 2026
e2db76a
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Mar 15, 2026
17d6d25
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Mar 18, 2026
081e010
Merge branch 'main' into sanjaiyan-react-html-optmize
sanjaiyan-dev Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tough-days-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/react': patch
---

Refactors to improve the performance of rendering static HTML content in React
16 changes: 7 additions & 9 deletions packages/integrations/react/src/static-html.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createElement as h } from 'react';
import { createElement as h, memo } from 'react';

/**
* Astro passes `children` as a string of HTML, so we need
Expand Down Expand Up @@ -26,12 +26,10 @@ const StaticHtml = ({
};

/**
* This tells React to opt-out of re-rendering this subtree,
* In addition to being a performance optimization,
* this also allows other frameworks to attach to `children`.
*
* See https://preactjs.com/guide/v8/external-dom-mutations
* React.memo is the modern functional equivalent of shouldComponentUpdate.
*
* By returning `true` in the comparison function (the second argument),
* we tell React that the props are "equal" and it should skip re-rendering,
* effectively making this subtree static.
*/
StaticHtml.shouldComponentUpdate = () => false;

export default StaticHtml;
export default memo(StaticHtml, () => true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be surprised if it worked, undocumented

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Screenshot 2026-03-07 at 14 48 18
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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🚀

Astro js with react js benchmark(memo vs  unoptimised) screenshot
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!

Loading