Qwik SEO

How Qwik's resumability puts content in the HTML by default and drives near-zero INP and TBT — plus the QwikCity patterns for meta tags, canonical URLs, JSON-LD, sitemaps, and i18n that make a Qwik site rank.

First published: Jun 26, 2026 · Last updated: Jul 18, 2026 · Advanced
demand #8 in JavaScript Frameworks#41 in Platform SEO#271 in Technical SEO#378 on the site
1 evidence signal on this page

Qwik is a JavaScript framework whose core trick is resumability: the server serializes app state, listeners, and the component tree into the HTML, so the browser resumes without hydrating (a ~1 KB Qwikloader script still runs — it's not literally zero JS). For SEO that's a double win on routes that are actually server-rendered — content is in the HTML by default (crawlers see it immediately, no render-wave delay) and INP/TBT trend toward zero because no hydration blocks the main thread on load; confirm both with field data rather than assuming them. QwikCity, the meta-framework, lets you mix SSR and SSG per route, plus routeLoader$() for server-side data and a head export for title/meta/OG/canonical/JSON-LD — all server-rendered. Basic SEO works out of the box; the main discipline is using routeLoader$() for dynamic metadata, not client-side effects. The catch is a smaller ecosystem than React/Vue, and — as of this writing — Qwik is on its v2 line, currently in beta.

TL;DR — Qwik’s core innovation is resumability: at SSR time it serializes event listeners (as HTML attributes), the component tree (in HTML comments), and app state (in a qwik/json script) into the HTML, so the browser resumes instead of hydrating — a ~1 KB Qwikloader script still runs, so it’s not literally zero JS. That’s categorically different from React/Vue/Angular/SolidJS, which all hydrate. On routes that are actually server-rendered, the SEO payoff is twofold: content is in the HTML by default (no render-wave delay for crawlers) and INP/TBT trend toward zero because no hydration blocks the main thread on load — verify both with field data rather than assuming them. QwikCity is the meta-framework — SSR and SSG can be mixed per route, not chosen once for the whole project. Metadata lives in the head export (title, meta, OG, canonical, JSON-LD), fed by routeLoader$() for server-side data. Sitemaps are automatic in SSG (for routes actually built) and a dynamic route in SSR. Qwik is currently on its v2 line (beta) as of this writing. The honest caveat: the ecosystem is newer and smaller than React’s.

Resumability: the core idea (and why SEO cares)

Qwik was built by Miško Hevery — the creator of Angular — at builder.io, and its whole reason to exist is to kill hydration. (As of this writing, Qwik is on its v2 line, currently in beta — check the API surface against your installed version before copying code from any source, including this one.) Hydration is what every mainstream framework does after SSR: it downloads the component code and re-executes it in the browser to attach event handlers and rebuild internal state. As Hevery puts it, “Hydration is when an application is downloaded and executed twice, once as HTML and again as JavaScript.” That second execution is pure overhead — the page looks ready but isn’t interactive, and the main thread is blocked.

Qwik instead serializes three things into the HTML at SSR time:

  • Event listeners — encoded as HTML attributes (e.g. on:click="./chunk.js#symbol"). A tiny inline script (the Qwikloader, ~1 KB) attaches one global listener, reads the encoded chunk/symbol on an event, and downloads only that handler code on demand.
  • Component tree structure — encoded in HTML comments, so Qwik can rebuild the hierarchy without executing component code.
  • Application state — serialized into a <script type="qwik/json"> block, so any component can resume without its parent being present.

When the page loads, Qwik doesn’t re-run your components. Evidence for this claim Qwik resumability restores listeners and application state without rerunning component initialization on page startup. Scope: Qwik resumability model. Confidence: high · Verified: Qwik: Resumable As the docs put it, “Resumability is a way for a framework to recover its state without re-executing the application components on the client.” The result, in the docs’ words: “Qwik apps work instantly without any delay because they don’t need hydration, regardless of their size or complexity.”

This is not Islands Architecture (Astro). Islands still hydrate each island individually. Qwik hydrates nothing — there’s no per-island hydration cost because there’s no hydration at all.

What crawlers actually receive

Because Qwik pre-renders everything server-side and never re-renders on the client, the HTML Googlebot fetches already contains your complete content — text, links, headings, the lot — for a route that’s actually rendered on the server. Rendering is a per-route decision in QwikCity (SSR, SSG, or a mix); a route left in client-only mode, or content fetched from a client-side effect instead of routeLoader$(), doesn’t get this benefit just because the project uses Qwik. Google processes JavaScript in two waves (HTML first, then a deferred render pass that can lag hours to days). A heavily client-rendered app risks content only showing up in that second wave. A properly server-rendered Qwik route sidesteps that risk: there’s no render-queue delay because the content was never waiting on client JS in the first place. Treat “Qwik pages behave like static HTML for crawlers” as the architecture’s intent, then confirm it on your deployed URLs with view-source — the safest posture for JavaScript SEO is verifying the response, not trusting the framework label.

Bing works the same way — a two-wave model that prefers server-rendered HTML for efficiency. Qwik’s HTML-first output benefits both engines for the same reason.

Core Web Vitals: a structural advantage

Google uses field data (CrUX) for the Page Experience signal, and INP replaced FID as a Core Web Vital in March 2024. Here’s where Qwik’s architecture pays off directly: with the Qwikloader as the only script running on load — the docs note sites “can boot with about 1kb of JS (regardless of application complexity)” — there’s almost nothing to block the main thread. Total Blocking Time trends toward zero and INP stays low because there’s no hydration pass competing with the user’s first interactions. A bigger Qwik app doesn’t mean a bigger startup bill; the JavaScript is fetched lazily, per interaction, not as one boot bundle.

That’s the difference from the established frameworks. Next.js, Nuxt, and Angular SSR all put content in the HTML too — they’re not bad for SEO — but they carry a hydration cost that scales with the app. Qwik removes that cost by design.

That’s the architectural expectation, not a measured result. It doesn’t guarantee a specific INP, TBT, or CrUX score for your site — third-party scripts, analytics tags, and per-route data-fetching cost can all eat into the advantage. Confirm it with field data (CrUX, Search Console’s Core Web Vitals report) on your own deployed URLs before making a performance claim to a client or stakeholder; see the How to Measure tab for the specific KPIs and cadence.

Meta tags and the document head

In QwikCity, page metadata lives in a head export from each route file. For static metadata it’s a constant: Evidence for this claim Qwik City route modules can export a DocumentHead value or function for route metadata. Scope: Qwik City route metadata. Confidence: high · Verified: Qwik City: Head

export const head: DocumentHead = {
  title: 'Qwik SEO Guide',
  meta: [
    { name: 'description', content: 'How Qwik resumability helps SEO.' },
    { property: 'og:title', content: 'Qwik SEO Guide' },
    { property: 'og:description', content: 'Resumability, meta tags, sitemaps.' },
  ],
  links: [
    { rel: 'canonical', href: 'https://example.com/qwik-seo/' },
  ],
};

All of this is rendered server-side into <head>, so it’s in the HTML the crawler sees. The canonical pattern — links: [{ rel: 'canonical', href: '...' }] — is easy to miss but is the right place for it.

For dynamic metadata (blog posts, products), the SEO-critical pattern is routeLoader$(). It runs server-side before the HTML response, and the head export can be a function that receives the resolved loader value:

export const usePost = routeLoader$(async ({ params }) => {
  return await getPost(params.slug); // runs on the server
});

export const head: DocumentHead = ({ resolveValue }) => {
  const post = resolveValue(usePost);
  return {
    title: post.title,
    meta: [{ name: 'description', content: post.excerpt }],
    links: [{ rel: 'canonical', href: `https://example.com/blog/${post.slug}/` }],
  };
};

This is QwikCity’s equivalent of getServerSideProps. The discipline that matters for SEO: fetch metadata data with routeLoader$(), not in a client-side effect (useSignal()/useResource$() that run in the browser). If you set the title from a client effect, it won’t be in the first-wave HTML.

Structured data (JSON-LD)

JSON-LD goes in the head export’s scripts array, rendered inline in <head> server-side and fully crawlable:

export const head: DocumentHead = ({ resolveValue }) => {
  const post = resolveValue(usePost);
  return {
    title: post.title,
    scripts: [
      {
        props: { type: 'application/ld+json' },
        script: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'Article',
          headline: post.title,
          datePublished: post.date,
        }),
      },
    ],
  };
};

Because it’s serialized into the HTML response, there’s no rendering delay before the markup is available to crawlers. One setup detail that’s easy to miss: the scripts field only produces output if your project’s router-head component actually renders head.scripts (typically via dangerouslySetInnerHTML) — it’s not automatic just because you populated the array. If a starter template’s router-head.tsx doesn’t already loop over head.scripts, add that before trusting the JSON-LD shows up in the response.

Sitemaps and robots.txt

  • SSG builds: the sitemapOutFile config option auto-generates sitemap.xml during the static build — but it only includes routes that were actually built. A route excluded from the static build (dynamic params you didn’t pre-render, a page gated behind a condition) won’t be in the sitemap even though it exists.
  • SSR sites: there’s no automatic sitemap — add a route at src/routes/sitemap.xml/index.ts with a RequestHandler that builds and returns the XML dynamically.
  • robots.txt: drop it in /public/ so it’s served at the root.
  • Canonical and sitemap URLs both depend on origin. QwikCity builds absolute URLs from the configured origin/base (or a forwarded-header origin in SSR middleware). A wrong or leaked preview/staging origin in that configuration produces wrong canonicals and sitemap entries in production — check the actual output, not just the config file, after any adapter or deployment change.
Evidence for this claim The SSG origin and base configuration affect absolute canonical and sitemap URLs; preview or deployment origins can leak into production output if adapter configuration is wrong. Scope: production build, server adapter, origin reconstruction and caching Confidence: high · Verified: Deployments

International SEO

Two mechanisms, used together:

  • rewriteRoutes in QwikCity config maps localized URL paths to your routes without duplicating components (e.g. /it/documentazione//docs/). This gives you clean, localized URLs — the foundation for an international URL structure.
  • Qwik Speak (a separate library) handles content translation.
  • hreflang tags go in the head export’s links array, pointing each locale at its alternates. See international SEO and hreflang for the cross-engine rules.

SSR vs. SSG for SEO

Both put content in the HTML, so both are crawler-safe — and QwikCity lets you mix modes per route, not just once for the whole project. Choose per route by content shape:

  • SSG (static adapter) produces pure HTML files, no server needed — ideal for content that doesn’t change per request. Automatic sitemap (for routes that were actually built), cheapest hosting, fastest TTFB from a CDN. The tradeoff is freshness: content only updates on the next build, and a route that fails to build simply isn’t there.
  • SSR renders per request — needed for personalized or frequently changing data. Run it at the edge (Cloudflare, Vercel, Netlify) for low TTFB, and set Cache-Control headers on SSR responses so the CDN can serve repeats without re-rendering. The tradeoff is that a failure at request time (a slow or errored routeLoader$()) affects the live response, so add error handling for the data fetch rather than letting it surface as a broken page.

Neither mode is “more SEO-safe” than the other by default — pick per route based on how often the content changes and what happens if the data source is briefly unavailable, then verify the deployed response either way.

What to watch out for

  • Not everything serializes. Class instances, promises, and streams can’t be put into the HTML state. Code that touches them has to run client-only — keep it out of anything that needs to be in the server HTML (especially metadata). If you deliberately exclude a value with noSerialize(), remember it comes back as undefined after the app resumes on the client — don’t reach for it on anything the page still needs to read post-resume, including metadata paths.
  • useVisibleTask$() is a real escape hatch, use it sparingly. It runs eagerly on the client after initial render — the one hook that deliberately works against resumability. It doesn’t block rendering or hurt the initial HTML, but it does mean client JS is running that Qwik would otherwise have deferred. Fine for genuinely client-only work (a chart library, a map); wrong choice for anything that should be feeding server-rendered metadata.
  • Verify meta tags are in the source, not injected by JS. View-source (not just DevTools’ rendered DOM) should show your title, canonical, and JSON-LD. If they only appear in the rendered DOM, you set them from a client effect instead of the head export / routeLoader$().
  • routeLoader$() vs. useResource$()/useSignal(). Loaders run on the server and feed the head; the others can run in the browser. For anything that must be in metadata or first-wave HTML, use the loader.
  • Smaller ecosystem. Qwik has far fewer CMS connectors and SEO plugins than Next.js, and a smaller community to draw on. The framework is capable; you’ll just write more glue yourself. builder.io’s own site runs on Qwik, so it’s used in production — but go in eyes-open about the maturity gap.

Where this sits

Qwik is one of two frameworks in the JavaScript frameworks story — the other being SolidJS. The key distinction to keep straight: Qwik resumes; SolidJS (and React, Vue, Angular) hydrate. Both still output content-rich HTML via their meta-frameworks, so crawling is fine either way; the resumability angle is what makes Qwik’s INP/TBT profile structurally the best of the group. For the metric side, see Core Web Vitals; for the foundations, the JavaScript SEO hub.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin an expert quote first.