React server-side rendering and hydration
How to implement Optimizely Web Experimentation within React applications using server-side rendering and hydration
You can implement Optimizely Web Experimentation within React applications using server-side rendering (SSR) and hydration, including those built with frameworks like Next.js and Gatsby. However, the interaction between Optimizely's DOM manipulation and React's hydration process can cause technical challenges.
Challenges
While offering performance benefits, SSR adds complexity when you use it with client-side A/B testing tools like Optimizely. The core issue is the timing conflict between Optimizely's experiment modifications and React's hydration mechanism.
- Server-side rendering (SSR) – Frameworks like Next.js and Gatsby pre-render React components on the server, generating the initial HTML payload sent to the browser. This improves perceived performance and SEO.
- Hydration – Upon receiving the server-rendered HTML, the client-side React library "hydrates" the application. This involves reconciling the existing DOM with React's virtual DOM, attaching event listeners, and making the page interactive.
When an Optimizely experiment is active, the snippet modifies the DOM to present variations. However, with SSR, these modifications often occur before React's hydration process begins. This sequence leads to problems.
- The server generates the HTML.
- If set to run immediately, the Optimizely snippet applies visual changes based on active experiments, tagging modified elements with a unique UUID.
- The browser receives the HTML, which may include Optimizely's modifications.
- React's hydration process starts. It compares its virtual DOM (constructed based on the server-side render) with the actual DOM (potentially modified by Optimizely).
- React detects discrepancies. It attempts to reconcile the DOM to match its virtual DOM, often overwriting Optimizely's changes because it is unaware of the experiment modifications, and the Optimizely snippet believes the page has already been activated and had the changes applied.
This conflict typically manifests as flickering or flashing and lost or inconsistent changes.
Solutions
Force reactivation (deactivate/reactivate) post-hydration
This is the recommended way to handle Optimizely experiments within SSR and hydration environments. It uses a code snippet to reevaluate targeting conditions and reapply changes after React hydration.
Advantages
- Reliability – Ensures experiment changes persist post-hydration.
- Minimal flicker – Happens swiftly after React's execution.
- Straightforward implementation – Is relatively easy to integrate into SSR frameworks.
Considerations
- Code dependency – Requires adding custom code to the application.
- Strategic placement – Requires correct code placement within a root-level component to be effective.
Technical implementation
- Deactivate Optimizely pages – Programmatically deactivate all active Optimizely pages using
window.optimizely.push({ type: 'page', pageName: ..., isActive: false })
. This signals Optimizely to disregard any prior modifications. - Reactivate Optimizely pages: – Reactivate the same pages using
window.optimizely.push({ type: 'page', pageName: ... })
immediately after deactivation. Optimizely then reevaluates page targeting and reapplies experiment variations based on the current DOM state. The snippet recognizes that those elements do not have the UUID and reapplies the visual changes.
Code placement
This code must execute after React hydration and should reside high in the component tree to ensure it affects all pages and route transitions.
- Next.js – Within the
useEffect
hook of yourpages/_app.js
component. - Gatsby – Inside the
wrapRootElement
API ingatsby-browser.js
.
Code example
import { useEffect } from 'react';
function MyRootComponent({ children }) {
useEffect(() => {
if (typeof window !== 'undefined' && window.optimizely) {
const optlyPages = window.optimizely.get('data').pages;
Object.keys(optlyPages).forEach((pageId) => {
// Deactivate page
window.optimizely.push({
type: 'page',
pageName: optlyPages[pageId].apiName,
isActive: false,
});
// Reactivate page
window.optimizely.push({
type: 'page',
pageName: optlyPages[pageId].apiName,
});
});
}
}, []);
return <>{children}</>;
}
export default MyRootComponent;
Use dynamic websites "DOM Change" triggers
Web Experimentation's dynamic websites functionality uses MutationObservers to detect DOM modifications and automatically reapply experiment changes. However, its efficacy within the context of hydration can be inconsistent.
Technical Details
Mutation Observers are a browser API that let you observe changes within the DOM tree. Optimizely uses them to trigger the reapplication of experiment modifications when relevant DOM mutations are detected.
Limitations with hydration
- Timing sensitivity – React hydration often involves rapid, batched DOM updates. MutationObservers might not capture all subtle changes, leading to missed reapplications. This can be due to the asynchronous nature of both hydration and MutationObserver callbacks.
- Flicker potential – If the detected changes are substantial, flicker may occur as React hydrates first, and then Optimizely reacts to the subsequent DOM mutations.
Recommendation
Combine the dynamic websites feature with the force reactivation method for complex SSR applications or those with frequent dynamic updates. This provides a fallback mechanism and ensures more comprehensive coverage.
Reduce discrepancies between SSR and client-side state
This minimizes the differences between the server-rendered HTML and React's initial client-side state. While perfect parity is often unattainable with experiments, reducing unnecessary discrepancies can mitigate issues.
Implementation
Keep your server-rendered HTML as close as possible to the client-side state that React expects. This reduces the extent of DOM manipulation required during hydration, reducing the chances of conflicts with Optimizely's changes.
Benefits
- Reduced flicker – Less DOM reconciliation by React translates to less flicker.
- Simplified debugging – Fewer inconsistencies between server and client states make debugging easier.
Limitations
Experiments inherently introduce differences between server and client renders, making complete alignment difficult.
Best practices
Regardless of your chosen solution, follow these best practices:
- Load the Optimizely snippet early – Place the Optimizely snippet as high as possible within the
<head>
of your HTML document. This enables the snippet to process and apply changes earlier in the page load cycle. - Use a root-level hook for reactivation – When forcing reactivation, ensure the logic resides within the highest-level component possible, such as
_app.js
inNext.js
and the root element in Gatsby. This guarantees its execution across all pages and route changes. - Use a staging environment – Thoroughly test your SSR + hydration implementation in a staging environment before you deploy to production. Verify that experiments render correctly with minimal to no flicker.
- Minimize large layout shifts – Significant layout shifts from experiments increase flicker during hydration. Use placeholders or skeleton states to maintain a consistent layout during the loading phase, which reduces perceived visual disruption.
- Monitor performance – While the overhead of
optimizely.push()
is minimal, monitor its usage, especially if you have many pages or frequent re-renders. This is crucial for applications with stringent performance requirements.
Complete Next.js example
This demonstrates force reactivation within a Next.js application.
pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
{/* Optimizely Web snippet */}
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
pages/_app.js
import { useEffect } from 'react';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
useEffect(() => {
if (typeof window !== 'undefined' && window.optimizely) {
const optlyPages = window.optimizely.get('data').pages;
for (const pageId in optlyPages) {
window.optimizely.push({
type: 'page',
pageName: optlyPages[pageId].apiName,
isActive: false,
});
window.optimizely.push({
type: 'page',
pageName: optlyPages[pageId].apiName,
});
}
}
}, []);
return <Component {...pageProps} />;
}
export default MyApp;
Alternative: Control activation with markers
An alternative approach to mitigating SSR and hydration issues uses markers to control when and where Optimizely activates experiments. This method gives you more granular control over the activation process and lets you specify which components should be targeted and when, further minimizing potential conflicts with React's hydration. See Use markers to control activation for information.
Updated 6 days ago