Astro Page Speed: How This Site Reached a 100/100 Score
This Astro site scored 100/100 because of four changes, not one: working automated deploys, HTML cached at the Cloudflare edge, CSS inlined into every page, and images sized to their real display width. Time to first byte from Europe fell from 654ms to 126ms. Astro made the fast baseline easy, but the last 40 points came from delivery, not the framework.
Most page speed articles are written about a demo site. This one is about the site you are reading, with the numbers taken from real scans over one month. If you want to check the claims, run tanvirtusher.com through PageSpeed Insights yourself. It takes thirty seconds.
Where did it start?
The site was already built with Astro as a static site, with self-hosted fonts, WebP images and almost no JavaScript. On paper it should have been fast. The first scan disagreed.
| Scan | Server response and caching | Time to first byte (from Europe) |
|---|---|---|
| 31 Aug 2026 | F, 40/100 | 654ms |
| 25 Sep 2026, before the fixes | D, 63/100 | 568ms |
| 25 Sep 2026, after the fixes | A+, 100/100 | 126ms |
The final desktop scan scored 100/100 overall (97 on the mobile run), with Lighthouse at 99 on desktop and 98 on mobile, largest contentful paint at 242ms, total blocking time at 0ms and a total page weight of 0.4 MB.
The interesting part is not the final score. It is that a correctly built static site started with an F.
Why was a static Astro site slow?
Because the framework was never the problem. The scanner measured the server responding in 2ms from Google’s own network, yet visitors in Europe waited 654ms for the first byte. The difference was distance.
Every HTML request travelled from the visitor to the hosting server and back, because Cloudflare was only passing HTML through. Its response header said cf-cache-status: DYNAMIC on every page. Cloudflare caches images, CSS and scripts by default, but it does not cache HTML unless you tell it to.
One detail made this easy to miss. Testing from Dhaka, close to the server, gave a time to first byte of about 100ms and looked fine. Most of this site’s clients are in South Korea, and the scanner was measuring from Lithuania. Their experience looked like the 654ms figure. Always measure from where your users are, not from where you are.
Fix 1: make sure your deploys actually deploy
Before touching caching, I checked what was really live. The site deploys through GitHub Actions over FTPS, and the history showed 30 failed runs in a row. The upload step had never worked, because the FTP credentials were never added as repository secrets. Every “deploy” until then had been a manual zip upload.
The clue was on the server. Every HTML file had the exact same modification time, to the second. An incremental sync leaves each file with its own timestamp; a bulk extract stamps them all at once.
This matters for speed work in a specific way. Caching HTML at the edge is only safe if every deploy also clears that cache. That needs a pipeline that runs reliably, so this had to be fixed first. The workflow now builds the site, uploads it, and then calls the Cloudflare API to purge the cache.
Fix 2: cache HTML at the Cloudflare edge
This one change took the caching score from D to A+. In the Cloudflare dashboard, under Caching, then Cache Rules, one rule:
When: not starts_with(http.request.uri.path, "/api/")
Then: Eligible for cache
Edge TTL: use cache-control header if present
Browser TTL: respect origin
Three decisions are hidden in those lines:
- The contact form’s API is excluded. The form posts to a Cloudflare Worker at
/api/contact, and that must never be cached. - The origin still decides the lifetime. The server sends
max-age=3600, must-revalidatefor HTML and a one-yearimmutableheader for hashed assets. Cloudflare follows those rather than overriding them. - Purge-on-deploy makes the one-hour lifetime harmless. Without it, an update could take an hour to appear. With it, new content is live as soon as the upload finishes.
After the rule went live, pages returned MISS on the first request and HIT on every one after it. Time to first byte from Europe dropped to 126ms, and to 40 to 65ms from Dhaka.
Fix 3: inline the CSS
The asset score was stuck at C because of one Lighthouse audit: render-blocking resources. Each page linked two stylesheets, a shared one and a page-specific one. The browser will not paint anything until both have downloaded, which cost roughly 250ms of first paint.
The fix in Astro is one line in astro.config.mjs:
build: {
inlineStylesheets: 'always',
},
The default, 'auto', only inlines small stylesheets. Setting it to 'always' moves all CSS into a <style> tag in each page’s <head>, so there is nothing separate to wait for.
This is a trade-off, not a free win. Inlined CSS cannot be cached separately and is downloaded again with every page. It made sense here because the CSS was small: about 7 KB per page after compression, and every page still weighs 12 to 14 KB compressed, small enough to arrive in the first network round trip. If your CSS is 50 KB, a separate, cached file is probably still the better choice. I left a note in the config to revisit this if the CSS grows past about 20 KB.
Fix 4: size images to their real display width
One page looked blurry even though the screenshots on it were sharp 1440 by 720 pixel images. The cause was a mismatch between two numbers.
Astro’s <Image> component generates several sizes of each image, and the sizes attribute tells the browser which one to download. The attribute said the image would display at 420 pixels wide. The layout actually displayed it much wider, across the full content column. So the browser correctly fetched the small file, and then CSS stretched it.
The fix was a three-column grid, so each image really does display at about 365 pixels, with sizes updated to match:
<Image
src={screenshot}
alt="..."
widths={[365, 730]}
sizes="(max-width: 780px) 100vw, 365px"
/>
The rule worth remembering: sizes must describe the layout you actually have. It is a promise to the browser, and if the promise is wrong, the browser downloads the wrong file.
What did Astro do on its own?
A fair amount, and it is why the remaining work was about delivery rather than code.
- No JavaScript by default. The built site contains zero JavaScript files. The only scripts are small inline snippets for the dark mode toggle and the project filter on the work page. Total blocking time is 0ms because there is nothing to block.
- Static HTML. Every page is a prebuilt file, so the server has nothing to compute per visitor.
- Image processing at build time. WebP conversion, multiple widths and explicit dimensions, which is why layout shift stays low.
- Hashed asset filenames. Every file name changes when its content changes, so assets can be cached for a year without ever serving an old version.
The fonts were a manual decision. Three variable fonts are self-hosted and cut down to Latin characters, and only the heading font is preloaded, because it paints the largest element on most pages. Preloading the others would compete with it for bandwidth.
Does page speed improve SEO?
Somewhat, and it is worth being precise about how much.
Core Web Vitals are part of how Google assesses page experience. The thresholds for a good result are a largest contentful paint under 2.5 seconds, interaction to next paint under 200ms, and cumulative layout shift under 0.1. Interaction to next paint replaced first input delay as a Core Web Vital in March 2024, so guides that still talk about first input delay are out of date.
But Google has been clear that good page experience does not outrank relevance. A fast page that does not answer the search will not rank because it is fast. Speed works more like a tiebreaker between pages that are similarly useful.
The bigger effect is on people. Visitors leave slow pages before they finish loading, and that loss happens whether or not Google is watching. For a service business, a quick first impression is part of the pitch.
AI search features work the same way. A page has to be crawlable, indexed and able to appear as a snippet to be used in an AI answer. Speed helps a page get crawled efficiently, but it is the content that gets cited.
What is still not perfect?
A 100 is a score, not a finished job. Three things remain:
- Mobile largest contentful paint is 949ms, against 242ms on desktop. Still well inside the good range, but the gap is real.
- Layout shift is 0.075. Under the 0.1 threshold, but not zero. Something still moves after first paint.
- One image scored 50% on Lighthouse’s image format audit in the mobile run. A more efficient format such as AVIF would probably close it.
It is also lab data. The site is new, so Google does not yet have enough real-visitor data for the Chrome User Experience Report. Lab scores measure one simulated visit; field data measures real ones, and that is what Google uses. The lab results are a strong sign, not a guarantee.
A checklist you can use
If you run a static site, or an Astro site that feels slower than it should:
- Confirm your deploys actually deploy. Check what is on the server, not what the pipeline claims.
- Check the
cf-cache-statusheader on an HTML page. If it saysDYNAMIC, your HTML is not cached. - Add a cache rule for HTML, excluding any API routes, and purge the cache on every deploy.
- Measure time to first byte from where your users are.
- If your CSS is small, inline it with
inlineStylesheets: 'always'. - Make every
sizesattribute match the width the image really displays at. - Preload only the font that paints your largest element.
Common questions
Is Astro good for SEO?
Yes. It outputs static HTML with no JavaScript by default, so search engines get complete content without running scripts, and pages load fast. You still have to write the titles, descriptions, structured data and content yourself. Astro gives you a clean base, not the SEO.
Does Cloudflare cache HTML by default?
No. On default settings Cloudflare caches static files such as images, CSS and JavaScript, but passes HTML through to your server. You need a cache rule to cache HTML, and you should purge the cache whenever you deploy.
Should I inline all my CSS?
Only if it is small. Inlining removes a render-blocking request, but inlined CSS is downloaded again with every page instead of being cached once. Under roughly 10 to 20 KB compressed, inlining usually wins. Above that, a cached external file is often better.
What is a good time to first byte?
Google’s guidance treats 800ms or less as good. For a static site behind a CDN with HTML caching, 100 to 200ms from most regions is realistic, as this site now shows.
If your site is slower than it should be
This is the same process I use on client sites: measure what is actually live, find the one or two changes that move the numbers, and implement them rather than hand over a report. It is how technical SEO works here.
If you run an SMM panel, the problems are different. Panel templates tend to fail on indexing and duplicate service pages before speed even matters, which is what SMM panel SEO covers. And if you are starting a panel from scratch, it is cheaper to build it fast from the beginning than to fix it afterwards.
Either way, send me the URL and I will tell you the three things costing you the most before you pay anything.