SSR considerations
If you use Next.js, Remix, Astro, or any framework that renders on the server, the worry is
always the same: “will this analytics code crash the server render by touching window?” With
Funnel, the answer is no. This page explains why, and the one rule you need to follow.
Every client plugin guards on typeof window
Section titled “Every client plugin guards on typeof window”Browser analytics SDKs live on globals like window.gtag, window.fbq, or window.ttq. Those
globals don’t exist during server rendering. Every client plugin therefore checks for the
browser before touching anything:
// inside each plugin's track()track(eventName, params, context) { if (typeof window === "undefined" || !window.fbq) { return; // SSR or SDK not loaded yet → no-op, no error } // ...safe to call window.fbq here}The same guard applies to initialize(), setUser(), and setConsent(). The practical result:
a plugin called during SSR does nothing and throws nothing. It silently no-ops until it runs
in a real browser with the SDK present.
You can construct the Funnel during SSR
Section titled “You can construct the Funnel during SSR”Because of those guards, importing your singleton module and constructing new Funnel({...}) on
the server is completely safe. The constructor just stores the plugin list and doesn’t touch any
browser global. So this module is fine to import from server components:
// lib/funnel.ts (safe to import on the server)import { Funnel } from "@sunwjy/funnel-client";import { createGA4Plugin } from "@sunwjy/funnel-client/ga4";import { createMetaPixelPlugin } from "@sunwjy/funnel-client/meta-pixel";
export const funnel = new Funnel({ plugins: [createGA4Plugin(), createMetaPixelPlugin()],});The one rule: initialize on the client
Section titled “The one rule: initialize on the client”Even though plugins no-op on the server, there’s no reason to call initialize() or track()
there, as it would do nothing useful. So the rule is: construct anywhere, initialize and
track on the client.
In React-based frameworks, “on the client” means inside an effect, which never runs during server rendering:
"use client";
import { useEffect, useRef } from "react";import { funnel, pluginConfigs } from "@/lib/funnel";
export function FunnelProvider({ children }: { children: React.ReactNode }) { const initialized = useRef(false);
useEffect(() => { if (initialized.current) return; initialized.current = true; funnel.initialize(pluginConfigs); // runs only in the browser }, []);
return <>{children}</>;}This is exactly the pattern in the Next.js framework integration guide.
No errors during server render
Section titled “No errors during server render”Putting it together, here’s what happens across a server render and client hydration:
- Server render:
funnel.tsis imported andnew Funnel(...)runs. No browser globals are touched. Noinitialize()ortrack()fires (they’re inside effects). Nothing throws. - Client hydration: the effect runs,
initialize()boots the plugins, andtrack()calls now reach the realwindow.gtag/window.fbq.
If a track() somehow runs before the SDK has loaded, the typeof window/SDK guard makes it a
no-op rather than an error. You don’t lose the event, because events tracked before
initialize() are queued and replayed.
Where to go next
Section titled “Where to go next”- Framework integration: the concrete Next.js setup.
- Core concepts: the pre-initialize event queue.