The SaaS landing page that survives the traffic it asks for
Search for how to build a SaaS landing page and every result is about design: hero sections, social proof, a stunning gradient. Design advice is saturated. What nobody covers is what happens on launch day, when the page works and the traffic it was built to attract arrives all at once. Core Web Vitals under load, images, analytics that do not block the page, and signup forms that do not lose leads.
The launch post goes up at nine. By ten it is on the front page, and the landing page you spent three weeks on is doing exactly what it was designed to do: persuading people to click Start free trial. Which they do, four hundred a minute. The signup endpoint opens a database connection per request, the pool runs out, the form spins, and the people who were persuaded leave. The page was beautiful the whole time.
Search for how to build a SaaS landing page and the results are template marketplaces, design-tool vendors and agency portfolios, all about the same thing: the hero section, the social proof strip, the pricing table, the gradient. That advice is good and it is everywhere. What is almost nowhere is the engineering: what the page has to survive, measured in the numbers search engines and visitors actually judge it by.
The page is not what breaks
A landing page is mostly static content, and static content is the easiest thing on the internet to serve at scale. What breaks during a launch is everything the page sends people to:
| Component | What happens under a spike | The fix |
|---|---|---|
| Page rendered per request | Every visit hits your server and often your database, for content that is identical for everyone | Prerender to static HTML and serve it from a CDN |
| Signup or waitlist endpoint | Connection pool exhausted, slow responses, duplicate submissions from impatient double clicks | Store and return fast, idempotency key on the form, queue the rest |
| Transactional email | Provider throttles a new sending domain, confirmations land in spam or arrive an hour late | Warm the domain beforehand, SPF, DKIM and DMARC in place, send from a queue |
| CRM or marketing tool sync | Their API rate-limits you and the form errors, or the lead silently never arrives | Never call it during the request. Sync from your own table with retries |
| Social preview images | Every share is unfurled by several crawlers, and an image generated on demand runs each time | Pre-render the Open Graph images at build time |
| The app behind Start free trial | Onboarding jobs, seed data and welcome emails all start at once | Load test signup as a flow, and have a capacity fallback ready |
So the first rule is the least glamorous: load test the path a visitor takes, from the page through the form to the first screen of the product, not the landing page on its own. A landing page that serves ten thousand requests a second from a CDN tells you nothing about the endpoint behind its button.
Core Web Vitals, under real conditions
Google judges page experience with three field metrics, measured on real visits and assessed at the 75th percentile, so the slow quarter of your visitors decides the score:
- Largest Contentful PaintWhen the biggest element above the fold has rendered
- ≤ 2.5 s
- Interaction to Next PaintHow quickly the page responds to taps, clicks and typing
- ≤ 200 ms
- Cumulative Layout ShiftHow much the layout jumps while it loads
- < 0.1
The 75th percentile is the part a design review never sees. The designer's laptop on office fibre loads the page in under a second. A visitor arriving from a social app's in-app browser, on a mid-range phone and a weak mobile connection, is in that slow quarter, and launch traffic is disproportionately those visitors.
Largest Contentful Paint: usually the hero
- Never lazy-load the hero image. It is almost always the LCP element, and lazy loading tells the browser to wait. Load it eagerly with high fetch priority.
- Serve AVIF or WebP at the size it is displayed, with responsive sources for mobile. A 2,400 pixel PNG screenshot scaled down to a phone is the most common LCP failure I see.
- Prefer a poster image over an autoplaying hero video. If the video matters, show the poster as the LCP element and start the video after load.
- Self-host one or two font files, subset to the characters you use and preloaded. A heading that waits on a font from a third-party domain delays the paint that matters most.
Layout shift: reserve space for everything
Layout shift on landing pages comes from four places: images without dimensions, a web font swapping in at a different size, a cookie banner pushed in above the content, and an announcement bar injected by a script after load. Give every image explicit width and height, use a fallback font adjusted to the same metrics, and make banners overlay the page rather than push it down.
<!-- The hero: eager, prioritised, sized, in a modern format -->
<img
src="/hero-1280.avif"
srcset="/hero-640.avif 640w, /hero-1280.avif 1280w"
sizes="(max-width: 768px) 100vw, 1280px"
width="1280" height="720"
fetchpriority="high"
alt="The dashboard showing this week's bookings"
/>
<!-- Everything below the fold can wait -->
<img src="/feature-reports.avif" width="800" height="500" loading="lazy" alt="..." />Interaction to Next Paint: the third-party tax
A static landing page has almost no JavaScript of its own, so when INP fails it is nearly always somebody else's code: the tag manager, session recording, a chat widget, an A/B testing script that hides the page until it decides which variant to show, and a consent banner that loads a framework to render two buttons. Each is reasonable on its own. Together they hold the main thread while the visitor is trying to tap the button you paid to get them to.
Analytics that do not block the page
You need to know what the launch did. You do not need eight tools to tell you. Decide which numbers you will actually look at, which for most launches is visits by source, signups, and the conversion between them, and load the minimum that produces those:
- 1Count signups on the server, where the form is stored. That number is exact, cannot be blocked by an ad blocker and costs the page nothing.
- 2Use one lightweight analytics script for visits and sources, loaded after the page is interactive.
- 3Load chat widgets and session recording on interaction, for example when the visitor scrolls past the first section or opens the help button, rather than on every page view.
- 4Avoid client-side A/B testing that hides the page. Split variants at the CDN or at build time, so every visitor gets a complete page immediately.
- 5Collect your own field vitals with a small web-vitals reporter, so you see LCP and INP for launch-day visitors rather than waiting a month for the public report.
A form that does not lose leads
The form is the only part of the page that writes anything, so it is the only part that can lose something. The usual version calls the email provider, the CRM and a Slack webhook inside the request and only reports success if all three succeed. On a normal day that works. On launch day one of them is slow, the form spins, the visitor clicks again, and you get a duplicate or nothing.
Visitor submits the form
With an idempotency key generated when the page loaded
Honeypot and rate limit
Cheaper and friendlier than a CAPTCHA on the first try
↳ a bot or a flood: accept quietly, store nothing
Store the lead in your own database
Unique on email and idempotency key, so a double click is one row
Return success immediately
The visitor is done. Everything else happens without them
Queue: email, CRM, Slack
Each with retries and its own failure alert
The only thing in the request is your own database. Every third party is downstream of a row you already have.
export async function submitSignup(form: SignupForm, ip: string) {
if (form.website) return { ok: true }; // honeypot: bots fill every field
if (!(await rateLimit.allow("signup:" + ip, { perMinute: 5 }))) return { ok: true };
const lead = await db.leads.upsert({
where: { email: normaliseEmail(form.email) }, // a second submit is the same lead
create: { email: normaliseEmail(form.email), source: form.utmSource, key: form.idempotencyKey },
update: {},
});
await queue.publish("lead.created", { leadId: lead.id }); // email, CRM and Slack subscribe to this
return { ok: true };
}Two things to sort out before launch rather than during it. Set up SPF, DKIM and DMARC on the sending domain and send a few hundred real emails from it in the week before, because a brand new domain that suddenly sends thousands of confirmations is exactly what spam filters are built to catch. And decide what the button does if the product genuinely cannot take more users: a capacity waitlist that stores the lead and promises an invitation is a far better launch-day story than an error page.
The pre-launch checklist
- 1Prerender the page and confirm it is served from the CDN cache, not your server, by checking the response headers.
- 2Run a lab test on throttled mobile in CI, and add a field vitals reporter so you can see real visits on the day.
- 3Check the hero: eager, high priority, sized, AVIF or WebP, with responsive sources.
- 4Audit third-party scripts, remove what nobody will look at, and defer the rest until after interaction.
- 5Move every third party behind a queue in the signup path, with idempotency and retries.
- 6Warm and authenticate the sending domain.
- 7Pre-render the social preview images, and check how the link unfurls in the places you will post it.
- 8Load test the full signup flow at ten times the traffic you expect, and have the capacity fallback ready.
None of this is in tension with good design. The fastest landing pages I have built are also the simplest to look at, because every element that survives the checklist had to earn its place. This site is built the same way: every page prerendered, analytics loaded only after the page has finished loading, and contact through plain email and WhatsApp links rather than a form that can fail. If you are scoping the whole product rather than just the page, what a SaaS MVP actually costs puts the landing page and launch work in context, and the same reasoning about what is cheap to change later is in the architecture decisions that cost the most to reverse.
A landing page has one job on launch day: to still be working when the people it persuaded arrive.
If you have a launch coming and want the page, the signup path and the first screen of the product to hold up under the traffic you are hoping for, I build and review exactly that: static delivery, vitals in the field, queued lead capture and a load test of the real flow. Scoped and priced up front. Get a fixed-scope build quote.
Questions people ask
How do I build a SaaS landing page that loads fast?
Prerender it as static HTML served from a CDN, load the hero image eagerly with explicit dimensions and a modern format such as AVIF or WebP, self-host and subset one or two font files, and defer every third-party script until after the page is interactive. Then measure real visits, not just a lab score, because Core Web Vitals are judged on field data.
What breaks on a SaaS landing page during a Product Hunt or Hacker News launch?
Usually not the page. The signup endpoint and database take the real load, the transactional email provider throttles or lands in spam, the CRM API rate-limits the form, and social preview images rendered on demand get hit by every crawler that unfurls the link. Make the page static, queue everything behind the form, and pre-render the preview images.
Do analytics and chat widgets slow down a landing page?
Yes, and they are the most common cause of a failed Core Web Vitals report on an otherwise well-built page. Tag managers, session recording, chat widgets and consent banners compete with your own content for the main thread. Load them after the page is interactive or on first interaction, and prefer lightweight or server-side analytics for the numbers you actually use.
How do I stop a waitlist or signup form losing leads?
Write the submission to your own database first and return success as soon as it is stored. Send the confirmation email, CRM sync and Slack notification from a queue with retries, so a slow provider delays a step instead of losing the lead. Use a honeypot field and rate limiting before a CAPTCHA, and set up SPF, DKIM and DMARC before launch.
Building something like this?
I design and ship these systems for clients: retrieval over private data, agents that complete real tasks, and the Laravel platforms underneath them.