Core Web Vitals for Jekyll Sites: A Practical Optimisation Guide (2026)
How to measure and improve Core Web Vitals (LCP, INP, CLS) on your Jekyll site — with practical fixes for the most common issues.
Jekyll sites start with an inherent performance advantage — no database, no server-side rendering, no PHP. But Core Web Vitals are not just about server speed. Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) depend on how your HTML, CSS, JavaScript, images, and fonts are delivered to the browser.
Here is how to measure and fix each one.
Measuring Core Web Vitals
Before optimising, measure your current scores:
Google PageSpeed Insights — pagespeed.web.dev gives you both lab scores and real-world field data (Core Web Vitals from the Chrome User Experience Report).
Chrome DevTools — Open DevTools → Performance tab → record a page load. Check the LCP marker and layout shift events.
Web Vitals extension — Install the Google Web Vitals Chrome extension for real-time CWV scores as you browse your site.
Run tests on mobile, not just desktop. Mobile scores are what Google uses for ranking.
Largest Contentful Paint (LCP)
LCP measures how long it takes for the largest visible element to render. The target is under 2.5 seconds. For most Jekyll sites, the LCP element is either a hero image or the largest heading.
Fix 1: Preload your LCP image
If your LCP element is a hero image, add a preload hint in <head>:
{% if page.image %}
<link rel="preload" as="image" href="{{ page.image | relative_url }}">
{% endif %}
Fix 2: Use modern image formats
Convert images to WebP (25–35% smaller than JPEG) or AVIF (even smaller):
cwebp -q 85 hero.jpg -o hero.webp
Reference with a <picture> element for fallback:
<picture>
<source srcset="/assets/images/hero.avif" type="image/avif">
<source srcset="/assets/images/hero.webp" type="image/webp">
<img src="/assets/images/hero.jpg" alt="Hero" width="1200" height="630">
</picture>
Fix 3: Add width and height to images
This prevents layout shift and helps the browser calculate space before the image loads:
<img src="hero.webp" alt="Hero" width="1200" height="630" loading="lazy">
Do not use loading="lazy" on above-the-fold images — it delays the LCP element. Use loading="eager" or omit the attribute for hero images.
Fix 4: Self-host your fonts
Google Fonts adds a cross-origin request that delays rendering. Self-host fonts instead:
@font-face {
font-family: "Inter";
src: url("/assets/fonts/inter-v13-latin-regular.woff2") format("woff2");
font-display: swap;
}
The font-display: swap ensures text is visible while the font loads (using a system font fallback), preventing invisible text during load.
Fix 5: Eliminate render-blocking resources
CSS in <head> blocks rendering. JavaScript in <head> blocks rendering. Move non-critical CSS to inline critical styles, and add defer or async to all scripts:
<!-- Bad -->
<script src="/assets/js/main.js"></script>
<!-- Good -->
<script src="/assets/js/main.js" defer></script>
Interaction to Next Paint (INP)
INP replaced First Input Delay (FID) in 2024. It measures the time from any user interaction (click, tap, keyboard input) to the next frame painted. The target is under 200ms.
Jekyll sites with minimal JavaScript have excellent INP scores by default. Problems arise when you load heavy JavaScript that blocks the main thread.
Fix 1: Defer non-critical JavaScript
<script src="/assets/js/analytics.js" defer></script>
<script src="/assets/js/chat-widget.js" defer></script>
Fix 2: Avoid long tasks
Any JavaScript task that runs longer than 50ms can delay interactions. Use the Performance tab in Chrome DevTools to find long tasks (shown as red bars in the main thread timeline).
Break up long loops or calculations using setTimeout:
// Instead of one blocking loop:
function processItems(items) {
items.forEach(item => processItem(item)); // blocks if items is large
}
// Break it up:
function processItemsAsync(items, index = 0) {
processItem(items[index]);
if (index + 1 < items.length) {
setTimeout(() => processItemsAsync(items, index + 1), 0);
}
}
Fix 3: Remove unused JavaScript
Audit what JavaScript your site loads with the Chrome DevTools Coverage tab (DevTools → More tools → Coverage). Anything with high unused percentage is a candidate for removal or lazy loading.
Cumulative Layout Shift (CLS)
CLS measures unexpected layout movement — elements jumping around as the page loads. The target is under 0.1. Common causes on Jekyll sites:
Fix 1: Always specify image dimensions
<!-- Bad — no dimensions, layout shifts when image loads -->
<img src="hero.webp" alt="Hero">
<!-- Good — browser reserves space -->
<img src="hero.webp" alt="Hero" width="800" height="450">
Or use CSS aspect-ratio:
.post-image {
aspect-ratio: 16 / 9;
width: 100%;
}
Fix 2: Avoid inserting content above existing content
Announcement bars, cookie banners, and newsletter popups that appear after page load cause CLS. Either:
- Include them in the initial HTML (so they are rendered with the page, not injected later)
- Reserve space for them with a fixed height placeholder
Fix 3: Use font-display: swap and size-adjust
When a custom font loads, it can shift text because metrics differ from the fallback font. Use the size-adjust, ascent-override, and descent-override CSS properties to make your fallback match your custom font:
@font-face {
font-family: "Inter-fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
}
body {
font-family: "Inter", "Inter-fallback", sans-serif;
}
Fix 4: Avoid dynamically injected ads or embeds
Third-party embeds (Twitter, YouTube, Google Ads) that do not have reserved space cause significant CLS. Use placeholder containers with explicit dimensions:
<div style="aspect-ratio: 16/9; background: #f3f4f6;">
<!-- YouTube embed loads here -->
</div>
Jekyll-specific optimisations
Inline critical CSS
Extract the CSS needed to render above-the-fold content and inline it in <head>:
<style>
/* Critical CSS — only what is needed for above-fold content */
body { margin: 0; font-family: system-ui, sans-serif; }
.navbar { height: 64px; background: #fff; }
.hero { padding: 4rem 1rem; }
</style>
<link rel="preload" href="/assets/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
Compress Jekyll output with a plugin
Add jekyll-compress-html to minify your HTML output:
# Gemfile
gem "jekyll-compress-html"
# _config.yml
compress_html:
clippings: all
comments: all
endings: all
Target scores
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP | < 2.5s | 2.5–4s | > 4s |
| INP | < 200ms | 200–500ms | > 500ms |
| CLS | < 0.1 | 0.1–0.25 | > 0.25 |
A well-optimised Jekyll site can consistently achieve LCP under 1 second, INP under 50ms, and CLS of 0 — well above Google’s “good” threshold for all three metrics.
LCP: making your largest content element load fast
Largest Contentful Paint measures how quickly the largest visible element in the viewport loads. For most Jekyll blog posts, this is the cover image or hero image at the top of the page. For pages without images, it is often the first large paragraph of text.
The most impactful LCP optimisation is image format and size. Serving a cover image as a 2MB JPEG when it is displayed at 800×450 pixels is the single most common performance mistake on Jekyll blog sites. Convert images to WebP format (which is 25-35% smaller than JPEG at equivalent quality) and serve them at the correct dimensions. A 150KB WebP image at the display size loads dramatically faster than a 2MB JPEG that gets scaled by the browser.
Add fetchpriority="high" to the image element for your cover image and loading="eager" (rather than lazy) to ensure it is prioritised in the browser’s resource loading queue. For every other image below the fold, loading="lazy" defers the download until the user scrolls toward it — a combination that achieves fast initial load without sacrificing image quality on the rest of the page.
Font loading has a significant effect on LCP when the largest element is text. Web fonts that load slowly cause FOUT (Flash of Unstyled Text) or FCP (Flash of Invisible Text) before the text becomes visible, inflating LCP scores. Use font-display: swap in your @font-face declarations to show system fonts immediately while the web font loads. Better yet, self-host your font files in your Jekyll assets/fonts/ directory — self-hosted fonts load from the same origin as your HTML, eliminating a separate DNS lookup and connection to a font CDN.
INP: keeping your Jekyll site interactive
Interaction to Next Paint replaced First Input Delay as a Core Web Vitals metric in 2024. INP measures the delay between any user interaction — click, tap, keypress — and the next paint update. On a static Jekyll site with minimal JavaScript, INP scores are typically excellent because there is little JavaScript competing with user interactions on the main thread.
The Jekyll features most likely to cause INP problems are search functionality and interactive components added via JavaScript. Pagefind and Lunr.js both index processing off the main thread, but poorly optimised search implementations that perform synchronous filtering on large arrays during keystrokes can block the main thread and produce high INP scores. If you are building a custom search implementation, debounce the input event and keep the filtering logic lean.
Third-party scripts are the most common source of INP problems on Jekyll sites. Every third-party script — analytics, chat widgets, social sharing buttons, comment systems — runs on your users’ main threads and competes with interaction responses. Load third-party scripts asynchronously with the defer or async attribute, and avoid loading scripts that are only needed on specific pages on every page sitewide.
CLS: preventing unexpected layout shifts
Cumulative Layout Shift measures visual stability — how much page content moves unexpectedly after initial load. A CLS of zero means no visible content shifted; a CLS above 0.25 means users experienced significant layout instability, potentially clicking on the wrong element because a button moved while they were tapping.
The most common CLS causes on Jekyll sites are images without explicit width and height attributes, web fonts that cause text reflow on load, and dynamically injected content above the fold. Adding width and height attributes to every <img> tag allows the browser to reserve space for the image before it loads, preventing the text below from jumping down when the image appears. This single change eliminates image-related CLS entirely.
Web font CLS can be reduced by matching the fallback system font metrics to the web font’s proportions. The size-adjust, ascent-override, and descent-override CSS descriptors in @font-face allow you to tune the fallback font to minimise the visual shift when the web font swaps in. This technique, known as font metric override, reduces the visible shift from near-page-height to near-invisible in most cases.
Monitoring Core Web Vitals over time
Lighthouse and PageSpeed Insights give you a synthetic score at a point in time. Real-user Core Web Vitals data — measuring actual user experiences across a range of devices, network conditions, and browser versions — comes from the Chrome User Experience Report (CrUX), which feeds into Google Search Console’s Core Web Vitals report.
Check your Search Console Core Web Vitals report monthly. The data has a 28-day lag and aggregates across real users, so it reflects your actual user experience more accurately than any synthetic test. Pages marked as “Poor” in the report are the highest priority to optimise — they are receiving real users with genuine performance problems, and they may be experiencing ranking penalties as a result.
Set up Vercel Analytics or Plausible’s Web Vitals feature to monitor Core Web Vitals on an ongoing basis with your own user data. Both integrate with Jekyll sites easily and provide per-page performance data that Search Console’s aggregate view does not show. A post that is a Core Web Vitals outlier — loading much slower than the rest of your site — shows up immediately in these tools, allowing targeted optimisation rather than sitewide guessing.
A well-optimised Jekyll site with an image-heavy blog is capable of sustained 90+ performance scores across all three Core Web Vitals. The techniques in this post are sufficient to reach that level from a standard Jekyll theme baseline, and the resulting improvement in search ranking and user retention makes the optimisation investment worthwhile.