Home Blog Core Web Vitals for Jekyll Sites: A Practical Optimisation Guide (2026)
Tutorial

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.

Core Web Vitals for Jekyll Sites: A Practical Optimisation Guide (2026)

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 Insightspagespeed.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.

Share LinkedIn