DEV Community

Giulio Marinelli
Giulio Marinelli

Posted on

How I Built a 100/100 Lighthouse Landing Page with Qwik and Cloudflare Pages

Live page: giuliomarinelli.com
Tested on Mobile Lighthouse

I am currently rebuilding my personal website, but I did not want the domain to remain empty while the full project was still under construction.

The obvious solution would have been a minimal placeholder: a logo, a short message and perhaps a contact link. Instead, I treated the temporary landing page as the first production release of the future website.

It had to be:

  • multilingual;
  • accessible;
  • indexable;
  • visually stable;
  • inexpensive to deliver;
  • lightweight in the browser;
  • easy to evolve into the complete site.

The result is a small Qwik application deployed on Cloudflare Pages.

Lighthouse benchmarks

In a mobile Lighthouse run, the page reached:

Category Result
Performance 100
Accessibility 100
Best Practices 100
SEO 100
Agentic Browsing 3/3

The main synthetic performance metrics were:

Metric Result
First Contentful Paint 1.4 s
Largest Contentful Paint 1.4 s
Speed Index 1.5 s
Total Blocking Time 30 ms
Cumulative Layout Shift 0

Those scores are satisfying, but the interesting part is not the green circles themselves.

The interesting question is:

Where was unnecessary work removed?

The answer exists at two different layers.

Qwik reduces the amount of work required to start the application in the browser. Cloudflare reduces the distance, repeated transfers and origin work required to deliver it.

Less JavaScript. Less distance.

That is the idea behind this project.

The stack

At the time of writing, the landing page uses:

  • Qwik and Qwik City;
  • TypeScript;
  • Tailwind CSS;
  • Vite;
  • static generation for the localized pages;
  • a narrowly scoped Cloudflare Pages Function;
  • Cloudflare Pages for deployment and delivery.

The application has two canonical localized routes:

/it/
/en/
Enter fullscreen mode Exit fullscreen mode

Both pages are generated during the build. The root route is the only dynamic part:

/
Enter fullscreen mode Exit fullscreen mode

It reads the visitor's Accept-Language header and redirects to the most appropriate localized URL.

The architecture can be summarized as:

Request to /
    |
    v
Cloudflare Pages Function
    |
    +-- reads Accept-Language
    |
    +-- redirects to /it/ or /en/
                     |
                     v
              pre-rendered HTML
                     |
                     v
           Qwik resumes when needed
Enter fullscreen mode Exit fullscreen mode

This keeps runtime computation limited to the one place where it adds value.

Performance is a pipeline

It is tempting to describe web performance as a frontend-only concern. In practice, a page has to pass through several stages before a visitor can use it:

  1. the server or build system must produce the response;
  2. the response must travel through the network;
  3. the browser must parse the HTML and CSS;
  4. assets must be downloaded or recovered from cache;
  5. JavaScript may need to be downloaded, parsed and executed;
  6. the application may need to reconstruct client-side state;
  7. the browser must render a stable interface.

A fast framework cannot compensate for unnecessarily distant or repeatedly transferred assets. A powerful CDN cannot compensate for an application that blocks the main thread while booting a large client-side runtime.

Qwik and Cloudflare address different parts of the same path.

Server-rendered HTML does not eliminate startup cost

Server-side rendering and static generation can make content visible quickly because the browser receives useful HTML instead of an empty application shell.

However, in a hydration-based architecture, the browser often still needs to perform significant startup work before the application becomes interactive.

Conceptually, the process looks like this:

Server renders HTML
        |
        v
Browser downloads application code
        |
        v
Components execute again
        |
        v
The component tree, listeners and state are reconstructed
        |
        v
The page becomes interactive
Enter fullscreen mode Exit fullscreen mode

The server has already rendered the interface, but the client replays enough application logic to rebuild the framework's internal representation.

This reconstruction is hydration.

Hydration is not inherently wrong. It is a valid and widely used architecture. The trade-off is that its startup cost can increase with the amount of client-side code and the complexity of the page.

Qwik takes a different approach.

Qwik resumes instead of hydrating

The central idea behind Qwik is resumability.

Rather than rendering on the server and then rebuilding the application in the browser, Qwik serializes the information needed to continue execution from the state produced during server-side rendering or static generation.

That includes information related to:

  • event listeners;
  • component boundaries;
  • application state;
  • reactive relationships.

The browser does not need to eagerly execute every component merely to discover where listeners are located or how the component tree is structured.

A simplified mental model is:

Server renders and serializes the application
        |
        v
Browser receives ready-to-use HTML
        |
        v
Qwik resumes a specific part when an interaction requires it
Enter fullscreen mode Exit fullscreen mode

This distinction matters because the fastest startup task is often the task that never needs to run.

Qwik's own documentation describes resumability as pausing execution on the server and continuing it on the client without replaying all application logic:

Qwik: Resumable vs. Hydration

Why Qwik APIs end with $

A Qwik component commonly looks familiar to developers who have used JSX-based frameworks:

import { component$, useSignal } from "@builder.io/qwik";

export const Counter = component$(() => {
  const count = useSignal(0);

  return (
    <button onClick$={() => count.value++}>
      {count.value}
    </button>
  );
});
Enter fullscreen mode Exit fullscreen mode

The trailing $ is not decorative.

It marks a lazy-loadable boundary that the Qwik Optimizer can transform into an independently importable symbol. The component body and the click handler can therefore become separate entry points.

The generated HTML can contain a reference to the code needed for an interaction. Qwik calls that reference a QRL, or Qwik URL.

Conceptually, an event reference may resemble:

<button on:click="./chunk.js#handler_symbol">
  Click me
</button>
Enter fullscreen mode Exit fullscreen mode

A small global loader can observe the event, locate the referenced symbol and load the relevant code.

The browser does not need to download and execute the whole component tree at startup just to discover that one button has a click handler.

The Optimizer and QRL model are explained in more detail here:

Lazy loading without waiting for the click

Lazy loading code only after an interaction could introduce a visible delay. Qwik can mitigate this through targeted module prefetching.

The useful distinction is between:

  • downloading code;
  • executing code.

A likely-to-be-used module can be downloaded in the background and placed in the browser cache without being executed during application startup. When the interaction happens, the required code may already be available locally.

This allows Qwik to avoid eagerly executing application code while still preparing probable interaction paths.

The relevant mechanism is described in the official documentation:

Qwik: Prefetching Modules

How the landing page uses Qwik

The landing page is composed from small, focused sections:

export const LandingPage = component$<LandingPageProps>(({ locale }) => {
  const copy = LANDING_CONTENT[locale];

  return (
    <SiteShell locale={locale}>
      <HeroSection copy={copy.hero} />
      <AudienceSection copy={copy.audience} />
      <ProblemsSection copy={copy.problems} />
      <PreviewSection copy={copy.preview} />
      <OffersSection copy={copy.offers} />
      <MethodSection copy={copy.method} />
      <QualitySection copy={copy.quality} />
      <EditorialSection copy={copy.editorial} />
      <ContactCtaSection copy={copy.contactCta} />
    </SiteShell>
  );
});
Enter fullscreen mode Exit fullscreen mode

The Italian and English content share one strongly typed editorial structure:

export type LandingLocale = "it" | "en";

export const LANDING_CONTENT: Record<LandingLocale, LandingCopy> = {
  it: {
    // Italian content
  },
  en: {
    // English content
  },
};
Enter fullscreen mode Exit fullscreen mode

This does more than prevent missing fields.

It ensures that both languages follow the same content model while the visual components remain independent from the copy. The page can evolve without duplicating an entire route implementation for each locale.

The localized routes are pre-rendered during the build. This means that a visitor requesting /it/ or /en/ receives complete HTML without requiring a server-side render for every visit.

Qwik's resumability files are still emitted, so the page can become interactive without adopting a traditional full-page hydration step.

The other half of performance: delivery

Producing efficient HTML is only half of the problem.

The page still has to reach the visitor.

A content delivery network stores and serves content from geographically distributed infrastructure. Instead of every request travelling to one central origin server, a cacheable resource can be delivered from a location closer to the user.

This can reduce latency and reduce repeated work for the origin.

Cloudflare describes its cache as a globally distributed network of data centers that stores frequently accessed content closer to users:

Cloudflare Cache

For this project, Cloudflare Pages also removes the need to manage a traditional web server for the static output. A deployment publishes the generated site and its assets through Cloudflare's platform.

The effect is complementary to Qwik:

Qwik:
reduce work during browser startup

Cloudflare:
reduce delivery distance and repeated retrieval
Enter fullscreen mode Exit fullscreen mode

Edge cache and browser cache are not the same thing

When discussing caching, it is useful to separate at least two layers.

Edge cache

A Cloudflare data center may store a reusable response close to visitors.

Visitor
   |
   v
Cloudflare edge
   |
   +-- cache hit: return the stored response
   |
   +-- cache miss: retrieve the resource
Enter fullscreen mode Exit fullscreen mode

A cache hit can avoid another trip to the underlying storage or origin.

Browser cache

The visitor's own browser may store an asset locally.

Page requests asset
       |
       v
Browser cache contains a fresh copy
       |
       v
No network transfer is required
Enter fullscreen mode Exit fullscreen mode

Both layers reduce repeated work, but they operate in different places and are controlled by related but distinct policies.

Long-lived caching for versioned assets

The landing page defines explicit response headers for generated assets:

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/build/*
  Cache-Control: public, max-age=31536000, immutable

/fonts/*
  Cache-Control: public, max-age=31536000, immutable
Enter fullscreen mode Exit fullscreen mode

A one-year cache lifetime is appropriate only when the URLs are safe to treat as immutable.

Generated build assets normally include a content-derived identifier in the filename:

/build/q-abc123.js
/build/q-def456.js
Enter fullscreen mode Exit fullscreen mode

When the file content changes, the build emits a different URL. The old file can remain cached because the new HTML refers to the new filename.

This is the key principle:

Cache aggressively when the URL identifies the content.

The immutable directive tells the browser that the resource will not change during its freshness lifetime, avoiding unnecessary revalidation.

Cloudflare respects cacheable origin response headers such as public with a positive max-age, unless a cache rule changes the behavior:

Cloudflare: Default Cache Behavior

Not every resource should be cached for a year

The project also exposes text resources such as:

/llms.txt
/it/index.md
/en/index.md
Enter fullscreen mode Exit fullscreen mode

These keep stable URLs even when their content changes, so they use a shorter lifetime:

Cache-Control: public, max-age=3600
Enter fullscreen mode Exit fullscreen mode

That one-hour value is not universally correct; it is a project decision.

The important point is that cache policy follows resource semantics:

Resource type URL strategy Cache strategy
Hashed JS, CSS and assets URL changes with content Long-lived and immutable
Text endpoints with stable URLs URL remains constant Shorter TTL
Request-dependent redirect Response varies by visitor Do not store

The root route is deliberately not cached

The only dynamic route is /.

Its request handler reads the browser's language preferences and redirects to /it/ or /en/.

A simplified version is:

export const onRequest: RequestHandler = async (event) => {
  if (event.url.pathname !== "/") {
    await event.next();
    return;
  }

  const locale = resolveLocaleFromAcceptLanguage(
    event.request.headers.get("accept-language"),
  );

  event.headers.set("Vary", "Accept-Language");
  event.headers.set("Cache-Control", "private, no-store");

  throw event.redirect(302, `/${locale}/${event.url.search}`);
};
Enter fullscreen mode Exit fullscreen mode

The implementation also parses language quality values such as:

it-IT,it;q=0.9,en;q=0.8
Enter fullscreen mode Exit fullscreen mode

It selects a supported locale and falls back to Italian when no supported preference is available.

The response is intentionally marked:

Cache-Control: private, no-store
Enter fullscreen mode Exit fullscreen mode

That prevents a shared cache from storing a redirect generated for one visitor and serving it to another visitor with different language preferences.

Vary: Accept-Language documents that the response depends on that request header. The no-store directive provides the stronger guarantee required by this implementation.

Cloudflare's cache documentation confirms that private and no-store responses are not eligible for normal shared caching:

Cloudflare: Origin Cache Control

This produces a useful rule:

Cache everything that is safely reusable. Avoid caching the parts whose meaning depends on the individual request.

Static by default, dynamic by exception

The project is not entirely static and not generally server-rendered.

It is hybrid in a deliberately narrow way:

/          dynamic language negotiation
/it/       pre-rendered localized page
/en/       pre-rendered localized page
/assets/*  static, aggressively cached
/build/*   static, aggressively cached
/fonts/*   static, aggressively cached
Enter fullscreen mode Exit fullscreen mode

This pattern avoids turning a small request-dependent decision into permanent runtime work for every page view.

The server-side function decides where the visitor should go. The selected page itself does not need to be recomputed.

Less work at every stage

The complete performance strategy can be summarized as follows:

Stage Work avoided
Build Localized pages are rendered before requests arrive
Edge delivery Static output is distributed through Cloudflare
Shared cache Reusable assets do not require repeated retrieval
Browser cache Immutable assets do not need to be downloaded again
Browser startup Qwik avoids a traditional full hydration pass
Interactions Code can be loaded and executed at fine-grained boundaries
Rendering Stable dimensions prevent avoidable layout movement
Dynamic routing Runtime logic is confined to the root language redirect

No single row explains the Lighthouse score.

The result comes from repeatedly asking the same question:

Can this work be done earlier, done once, done closer to the visitor, deferred until necessary, or avoided completely?

What the Lighthouse score does not prove

A perfect Lighthouse score is useful feedback, but it has limits.

This result is a synthetic measurement from a relatively small landing page. It does not prove that:

  • every Qwik application will automatically be fast;
  • the page will behave identically on every real device and network;
  • a score of 100 guarantees good search rankings;
  • automated accessibility checks replace keyboard and screen-reader testing;
  • future analytics, forms or third-party scripts will have no performance cost.

Real-user monitoring matters after launch. Performance also needs to be measured again as the complete website grows.

The current result should therefore be read as evidence that the architectural baseline is healthy, not as a permanent certificate.

What I learned

1. A temporary page can be a real engineering exercise

A coming-soon page does not have to be disposable. It can validate deployment, internationalization, caching, SEO, accessibility and component architecture before the complete site is ready.

2. Static and dynamic are not binary choices

A single project can combine pre-rendered content with narrowly scoped runtime behavior. The useful question is not “static or dynamic?” but “which parts genuinely require runtime computation?”

3. Framework and infrastructure solve different problems

Qwik cannot move a data center closer to the visitor. Cloudflare cannot remove unnecessary client-side application startup.

Used together, they reduce work at complementary layers.

4. Caching should follow content identity

An asset whose URL changes with its content can be cached aggressively. A response that changes while keeping the same URL needs a shorter lifetime or no storage at all.

5. Performance is usually architectural before it is tactical

Image compression and bundle inspection matter. But the largest gains often come from earlier decisions:

  • pre-rendering instead of computing per request;
  • serializing state instead of reconstructing it;
  • loading code at granular boundaries;
  • distributing content close to visitors;
  • treating immutable and request-dependent resources differently.

Conclusion

Qwik and Cloudflare optimize different parts of the same journey.

Qwik reduces what the browser must execute to start and resume an application. Cloudflare reduces how far reusable content must travel and how often it must be retrieved.

The resulting speed does not come from one Lighthouse trick. It comes from systematically removing unnecessary work across the delivery pipeline.

The landing page is temporary.

The engineering principles behind it are not.

Have you compared Qwik’s resumability with a hydration-obased framework on a similar page? I’d be interested in seeing real-world measurements.

Top comments (0)