Agents fetch your page, parse it, extract facts, return to the LLM for the user's reply. Every second your page takes is a second the user waits for ChatGPT or Perplexity to answer. User-triggered browsing agents budget aggressively — typically 5-10 seconds total. If your page can't deliver useful HTML in that window, the agent gives up and tells the user "couldn't find it". This guide covers shrinking the render path.
| Agent type | TTFB target | FCP target | Total |
|---|---|---|---|
| Crawler (GPTBot, ClaudeBot) | < 2 s | n/a (raw HTML) | < 10 s |
| User browsing (ChatGPT-User) | < 800 ms | < 2.5 s | < 8 s |
| User browsing (Perplexity-User) | < 600 ms | < 1.8 s | < 5 s |
| Mobile agent over 4G | < 1 s | < 3 s | < 10 s |
curl -w "@-" -o /dev/null -s https://example.com/ <<'EOF'
time_namelookup: %{time_namelookup}s
time_connect: %{time_connect}s
time_appconnect: %{time_appconnect}s
time_pretransfer: %{time_pretransfer}s
time_redirect: %{time_redirect}s
time_starttransfer: %{time_starttransfer}s <-- this is TTFB
----------
time_total: %{time_total}s
EOF
// Bad: synchronous DB query before any HTML sends
const products = await db.query('SELECT ...'); // 800ms
return renderTemplate({ products }); // total TTFB: 800ms+
// Good: cached query + streaming
const products = await cache.getOrSet('products',
() => db.query('SELECT ...'),
{ ttl: 60 }
); // 5ms when cached
return renderTemplate({ products });
// Bad: blocking on third-party
const personalisedRecs = await fetch('https://thirdparty.com/api/recs');
// → adds 300-2000ms unpredictably
// Good: render without, hydrate after
return (
<Layout>
<ProductGrid /> {/* server-rendered */}
<LazyHydrate>
<PersonalisedRecs /> {/* loads client-side */}
</LazyHydrate>
</Layout>
);
<!-- Bad: JS in head, no defer/async --> <script src="/bundle.js"></script> <!-- Good: defer ensures HTML parses first --> <script src="/bundle.js" defer></script> <!-- Better: only ship what's needed --> <script src="/critical.js" defer></script> <script src="/secondary.js" defer async></script>
# Cloudflare page rule / cache rule Match: example.com/* Cache Level: Cache Everything Edge TTL: 1 hour (or longer for static content) Browser TTL: 10 minutes # Bypass for authenticated requests Cookie: session_id → bypass cache # Result: first byte from CDN edge (~50ms) # Origin only hit on cache miss or for logged-in users
Public marketing/content pages on CDN edge cache typically deliver TTFB < 100ms regardless of origin speed. Single most impactful fix.
See Fix JS-Only Content. SSR replaces "blank shell + JS fetch + render" with "HTML arrives with content". Saves a network round-trip + JS parse + JS execute = typically 1-3 seconds saved.
<!-- Lazy-load below-the-fold --> <img src="hero.webp" alt="..." width="800" height="400" /> <img src="..." loading="lazy" alt="..." width="..." height="..." /> <!-- Modern formats --> <picture> <source srcset="hero.avif" type="image/avif" /> <source srcset="hero.webp" type="image/webp" /> <img src="hero.jpg" alt="..." /> </picture>
For maximum speed, detect agent UAs and serve a stripped HTML version:
// Express middleware
app.use((req, res, next) => {
const ua = req.get('User-Agent') || '';
if (/GPTBot|ClaudeBot|PerplexityBot|CCBot/i.test(ua)) {
req.agentMode = true;
}
next();
});
// In route
if (req.agentMode) {
return res.render('product-lite', { product });
// Just title, description, price, schema. No JS, no images, no CSS.
// Renders in < 100ms even from origin.
} else {
return res.render('product-full', { product });
}
Not cloaking — the content is the same; only the delivery format differs. Allowed by Google and aligned with what agents need.
for ua in "GPTBot/1.0" "ClaudeBot/1.0" "PerplexityBot/1.0"; do time curl -s -A "$ua" https://example.com/ -o /dev/null done # All should complete in < 2 seconds for TTFB
Verify rendering completes within agent timeout windows.
Run Agent Compat →