Render at publish, serve from the edge: how a link in bio page shows up in 190 ms
I build Keepp, a link in bio and small website tool. A while ago we measured our published pages against real creator pages on six other link in bio platforms, on a phone-sized browser, on a fast connection and on 4G. Th
I build Keepp, a link in bio and small website tool. A while ago we measured our published pages against real creator pages on six other link in bio platforms, on a phone-sized browser, on a fast connection and on 4G. This post is the engineering side of that: what makes the difference, and a small script so you can measure your own pages.
The numbers, briefly
On 8 October 2026, median of three loads each, Largest Contentful Paint:
| Page | Fast connection | Fast 4G | Weight | Scripts |
|---|---|---|---|---|
| Keepp creator page | 0.19 s | 0.60 s | 0.28 MB | 3 |
| Stan Store creator page | 0.33 s | 1.04 s | 1.2 MB | 35 |
| Linktree creator page | 0.78 s | 1.62 s | 1.7 MB | 67 |
| bio.site creator page | 1.40 s | 49.8 s | 6.5 MB | 5 |
The full table, with ten pages, Carrd, Gumroad and Linkin.bio, is in the original write-up on the Keepp blog.
The 4G column is the one that matters for a link in bio page. Nearly every visit comes from a phone, usually from inside an app, often on a signal nobody picked.
Two ways to build the same page
A link in bio page has two very different audiences.
The creator edits it now and then, in a logged-in editor. That is a real app: state, previews, drag and drop. A client-rendered SPA is a perfectly good fit.
The visitor opens it once, for a few seconds, from a phone. They need a picture, a name and some buttons, as fast as possible.
A lot of link in bio tools serve both with the same architecture. The public page is a shell plus a JavaScript bundle that fetches the profile and builds the DOM on the phone. That is how you end up with 67 scripts to show a list of links, and why the time to first meaningful paint swings so much with the connection.
The alternative is to split them:
- Render once, at publish. When the creator presses Publish, the server builds the complete HTML for the page: layout, theme, text, links, image URLs. All the work that would otherwise happen on every visitor's phone happens once.
- Store the finished page at the edge. Keepp keeps that finished copy on Cloudflare's network, and a visit is served from the nearest location as a plain document. No app server, no database query and no framework boot on the request path.
- Rebuild on write. Any change that affects the public page (an edit, a price change, a product selling out) triggers a rebuild of that copy. Reads stay dumb and fast. The cost moves to writes, which are rare.
- Keep the critical path small. The page needs no script to paint. One small, deferred runtime script handles the interactive parts, like opening a product, a booking or a form, after the page is already on screen. In the benchmark that came to three scripts in total on the network.
None of this is a new idea. It is static site generation and CDN caching, applied to a category that mostly grew out of SPAs. The interesting part is mostly discipline: keeping every new feature out of the request path.
One property I didn't plan for but like a lot: because the stored copy is complete on its own, published pages keep opening even when our own servers are unreachable.
Measure your own page
Here is a stripped-down version of how the numbers above were taken. It uses Playwright with Chromium, an iPhone-sized context, and Chrome's network emulation for the 4G run. Each load gets a fresh context, and you keep the median.
// npm i -D playwright && npx playwright install chromium
const { chromium, devices } = require('playwright');
// Chrome DevTools' Fast 4G: 165 ms latency, 9 Mbps down.
const FAST_4G = {
offline: false,
latency: 165,
downloadThroughput: (9 * 1024 * 1024) / 8,
uploadThroughput: (1.5 * 1024 * 1024) / 8,
};
async function measureLcp(browser, url, network) {
const context = await browser.newContext({ ...devices['iPhone 13'] });
const page = await context.newPage();
if (network) {
const cdp = await context.newCDPSession(page);
await cdp.send('Network.emulateNetworkConditions', network);
}
await page.goto(url, { waitUntil: 'load', timeout: 120000 });
const lcp = await page.evaluate(() => new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
resolve(entries[entries.length - 1].startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
}));
await context.close();
return lcp;
}
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
(async () => {
const url = process.argv[2];
const browser = await chromium.launch();
for (const [label, network] of [['fast', null], ['fast 4G', FAST_4G]]) {
const runs = [];
for (let i = 0; i < 3; i += 1) runs.push(await measureLcp(browser, url, network));
console.log(`${label}: ${(median(runs) / 1000).toFixed(2)} s LCP (runs: ${runs.map((r) => Math.round(r)).join(', ')} ms)`);
}
await browser.close();
})();
Run it with node lcp.js https://your-page.example. For page weight, listen to page.on('response') and add up the body sizes, or read the transfer sizes from performance.getEntriesByType('resource').
A few things that tripped me up:
- Use a fresh context per run. A warm cache makes everything look great.
- Measure from one place and say where. Edge-served pages and origin-served pages react very differently to distance.
- Pick real pages, not the platform's own showcase. Showcase pages are often tuned in ways an ordinary page isn't.
Takeaways
If you're building anything where the public view is read far more often than it's written (a link in bio page, a menu, a portfolio, a landing page), it's worth asking whether the visitor's phone really needs to build it. Doing the work once at publish time and serving a finished page from the edge removed most of our performance problems before we had to optimise anything.
If you measure your own link in bio page with the script, I'm curious what you get. The full benchmark and method are here: The fastest link in bio page you can publish.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.