Encited

Prerender ready signal

How the renderer decides a page is ready#

Before taking a snapshot, Encited waits for three things:

  1. The event loop to go quiet, so queued JavaScript has finished running.
  2. Network requests to settle, including XHR and fetch calls your app makes after load.
  3. The page to stabilize, meaning the DOM has stopped changing.

When you need the ready signal#

Most sites need nothing beyond that. The flag is for content that arrives after the page already looks settled, or that never starts loading on its own during a render:

  • A request that only fires after a quiet gap, or polling that fills in later.
  • A widget that loads on scroll, such as a review list or a photo gallery.
  • Content that only mounts when someone clicks, such as an accordion, a tab, or a "show more" section.

A render is a page load with no user in it, so nothing scrolls and nothing clicks. Content that waits for either of those never enters the DOM, and the flag lets you load it eagerly and hold the snapshot until it is in.

How to implement#

Set the flag to false early in the page, then set it to true when your critical content has finished loading:

// Early, before your content loads
window.prerenderReady = false;

// Later, once the content is in the DOM
window.prerenderReady = true;

// window.htmlSnapshot works the same way

We wait up to 30 seconds for the flag. If it is never set to true, we snapshot the page as it stands when that timeout is reached.

Example in a React component:

useEffect(() => {
  if (dataLoaded && !isLoading) {
    // Signal that the page is ready for prerendering
    window.prerenderReady = true;
  }
}, [dataLoaded, isLoading]);

Example with async data fetching:

async function loadPageData() {
  try {
    const data = await fetchSlowAPI();
    setData(data);
  } finally {
    // Always signal ready, even on error
    // to prevent a full-timeout wait
    window.prerenderReady = true;
  }
}

Example waiting on a third-party widget:

window.prerenderReady = false;

new MutationObserver((_, obs) => {
  if (document.querySelector(".review-card")) {
    window.prerenderReady = true;
    obs.disconnect();
  }
}).observe(document.body, { childList: true, subtree: true });

Content behind a click#

A FAQ accordion, a spec tab, or a "show more" block usually mounts its content only when someone clicks it. Since a render has no user in it, that content never enters the DOM and so is not in the snapshot.

Open the panels yourself, wait for the content to appear, then set the flag. Detect the render with window.__ENCITED__ so real visitors keep the collapsed page:

if (window.__ENCITED__) {
  window.prerenderReady = false;

  document.addEventListener("DOMContentLoaded", () => {
    // Open every collapsed section
    document
      .querySelectorAll('[data-accordion-trigger][aria-expanded="false"]')
      .forEach((trigger) => trigger.click());

    // Wait for the panels to mount, then snapshot
    new MutationObserver((_, obs) => {
      const open = document.querySelectorAll('[data-accordion-panel]:not([hidden])');
      if (open.length) {
        window.prerenderReady = true;
        obs.disconnect();
      }
    }).observe(document.body, { childList: true, subtree: true });
  });
}

Swap the selectors for whatever your components use. The pattern is the same for tabs: render every panel instead of only the active one, then set the flag.

Important Always ensure the flag gets set, even if your data fetch fails. Otherwise the render waits the full 30 seconds before snapshotting.

TypeScript support#

Add these type declarations to avoid TypeScript errors:

// In a .d.ts file or at the top of your file
declare global {
  interface Window {
    prerenderReady?: boolean;
    htmlSnapshot?: boolean;
  }
}

Only use this if you notice content missing in your prerendered pages. Adding unnecessary delays can slow down your cache warm-up process.