How to Fix Core Web Vitals on HubSpot CMS

<span id="hs_cos_wrapper_name" class="hs_cos_wrapper hs_cos_wrapper_meta_field hs_cos_wrapper_type_text" style="" data-hs-cos-general-type="meta_field" data-hs-cos-type="text" >How to Fix Core Web Vitals on HubSpot CMS</span>

Your HubSpot site looks great in the page editor. Then you run it through PageSpeed Insights and the score comes back at 54 on mobile. You check the report, and it is full of terms like LCP, CLS, INP, render blocking resources, and a long list of scripts you did not add. Welcome to the reality of Core Web Vitals on HubSpot CMS.

Here is the thing most agencies will not tell you: a significant chunk of your performance overhead on HubSpot is platform level. HubSpot injects its own tracking scripts, chat widget code, form JavaScript, and analytics bundles on every single page. You cannot remove them. But that does not mean you are stuck with a slow site. There is plenty you can control, and the fixes are more straightforward than you might expect.

This guide covers what actually moves the needle on HubSpot CMS, what you are wasting time on, and where the platform itself is the bottleneck.

What Core Web Vitals Actually Measure (Quick Version)

Google uses three metrics to judge how your pages feel to real visitors:

  • LCP (Largest Contentful Paint) measures how fast the biggest visible element loads. Usually your hero image or heading. Target: under 2.5 seconds.
  • INP (Interaction to Next Paint) measures how fast the page responds when someone clicks, taps, or types. Target: under 200 milliseconds. This replaced FID in March 2024 and it is stricter. 43% of sites still fail it.
  • CLS (Cumulative Layout Shift) measures how much stuff jumps around while the page loads. Target: under 0.1.

Google evaluates these at the 75th percentile of real user data. That means 75% of your actual visitors need to have a good experience for the page to pass. Lab tools like Lighthouse give you a synthetic score, but Google ranks you based on field data from the Chrome User Experience Report.

The HubSpot Tax: Scripts You Cannot Remove

Every HubSpot CMS page loads a baseline set of scripts automatically. These include bundle.production.js, bundles/project.js, bundles/visitor.js, and bundles/app.js. They handle analytics tracking, the conversations widget, form rendering, and other platform features.

HubSpot says these load asynchronously and should not block rendering. That is technically true. But they still consume bandwidth, compete for the main thread on mobile devices, and show up as warnings in PageSpeed Insights. On a mid range Android phone over 4G, those scripts add real processing time.

You cannot remove them. You cannot defer them further. What you can do is make sure everything you control loads as efficiently as possible, so these platform scripts have less to compete with.

Fix #1: Stop Killing LCP With Unoptimized Hero Images

The single biggest LCP win on most HubSpot sites is the hero image. Content editors upload a 4000px wide photo straight from their camera roll or stock library, and HubSpot serves it as is. A 2MB hero image will blow your LCP budget every time.

What to do:

  • Keep hero images under 150 KB. Seriously. Compress before uploading using tools like Squoosh or TinyPNG.
  • Use HubL's resize_image_url function to serve the right size for each breakpoint. This function generates a URL for a resized version of your image on HubSpot's CDN. You still need to build the srcset attribute yourself:
<img
  src=""
  srcset=" 480w,
           800w,
           1200w,
           1600w"
  sizes="100vw"
  alt=""
>
  • Preload the hero image in your layout template's <head>. This tells the browser to start fetching the image before it even parses your CSS:
<link rel="preload" as="image" href="">
  • Do NOT lazy load your hero image. The loading="lazy" attribute tells the browser to wait until the image is near the viewport. Your hero is already in the viewport on load. Lazy loading it delays LCP.

Fix #2: Lazy Load Everything Else

Every image below the fold should use loading="lazy". HubSpot's image HubL tags now support the loading attribute natively. In custom modules, add show_loading and loading keys to the image field in fields.json, or just set it directly in your module HTML:

<img src="" alt="" loading="lazy">

This is free performance. Every image you lazy load is bandwidth and main thread time that gets shifted from initial page load to when the visitor actually scrolls to that content.

Fix #3: Move Your JavaScript to the Footer and Defer It

If your modules load JavaScript in the <head>, the browser has to download and parse that script before it can render anything. That directly hurts LCP and INP.

HubSpot gives you two ways to fix this:

In meta.json (for module.js):

{
  "js_render_options": {
    "position": "footer",
    "async": true
  }
}

In module.html (for external scripts):

The difference between async and defer: async downloads the script in parallel and executes it immediately when ready (execution order is not guaranteed). Defer downloads in parallel but waits until the HTML is fully parsed before executing (maintains order). For most module scripts, defer is what you want.

Fix #4: Stop Loading Libraries You Do Not Need

This one is rampant in HubSpot themes. A carousel module loads Slick Slider (jQuery dependency) on every page, even pages without a carousel. A lightbox module loads its CSS and JS globally through the theme's main stylesheet.

Load libraries conditionally. Only pull in resources when the module that needs them is actually on the page:

And stop using jQuery. Every interaction you build with jQuery can be done with vanilla JavaScript. jQuery adds 85 KB (minified) of code your visitors' browsers need to download and parse. On mobile that is a meaningful INP hit. Modern browsers have querySelectorAll, addEventListener, fetch, classList, and everything else you were using jQuery for.

Fix #5: Get Your Fonts Under Control

Fonts are a sneaky LCP killer. If your heading font has not loaded when the browser tries to render the hero, it either shows invisible text (flash of invisible text, or FOIT) or swaps in a fallback (flash of unstyled text, or FOUT). Both can delay LCP and cause layout shifts that hurt CLS.

Good news: HubSpot recently changed how Google Fonts load. If you select fonts through theme or module fields, HubSpot now serves them from your own domain using @font-face declarations instead of fetching from fonts.googleapis.com. This eliminates the third party connection overhead.

What you should still do:

  • Use font-display: swap in your @font-face rules. This tells the browser to show text immediately with a fallback font, then swap in the custom font when it loads. No invisible text, no LCP delay.
  • Preload your primary heading font weight in the template <head>:
<link rel="preload" as="font" type="font/woff2" href="/path/to/heading-font.woff2" crossorigin>
  • Limit your font weights. Every weight and style you load is another file the browser needs to fetch. Most sites need two weights per font family at most (regular and bold). Loading 300, 400, 500, 600, and 700 because they were available is a performance tax for zero design benefit.

Fix #6: Set Explicit Dimensions on Everything

CLS happens when the browser does not know how big something will be before it loads. The image pops in and pushes everything below it down the page. The font swaps and the heading changes height. An ad slot or embed loads late and shoves content sideways.

The fix is straightforward: tell the browser how much space to reserve.

  • Always set width and height attributes on <img> tags. The browser uses these to calculate the aspect ratio and reserve space before the image loads.
  • Use CSS aspect-ratio on image containers instead of padding hacks:
.image-wrapper {
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.image-wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
  • Never set height: 100% on image wrappers without a defined parent height. This is a guaranteed layout shift.
  • If you embed third party widgets (videos, maps, forms), wrap them in a container with a fixed aspect ratio so the page does not jump when the embed loads.

Fix #7: Reduce Third Party Script Bloat

After HubSpot's own scripts, the next biggest performance drain is usually the pile of third party tools your marketing team has added over the months. Google Tag Manager loading six tracking pixels. Hotjar. Drift or Intercom on top of HubSpot's own chat. A social proof widget. An accessibility overlay. Each one adds JavaScript that fights for the main thread.

Run a quick audit:

  • Open Chrome DevTools, go to the Network tab, filter by JS, and sort by size. Count how many scripts are loading that you did not write.
  • For each one, ask: is this actively used? Is the data being looked at? If nobody has opened the Hotjar dashboard in four months, remove the script.
  • If you must keep a tracking script, load it after the page is interactive. Use a setTimeout or trigger it on the first user interaction:
document.addEventListener('scroll', function loadAnalytics() {
  // Load your tracking script here
  document.removeEventListener('scroll', loadAnalytics);
}, { once: true });

Fix #8: Use CSS Custom Properties Instead of Inline Styles

Many HubSpot modules apply colors, spacing, and other dynamic values as inline styles on individual elements. The problem is that inline styles cannot be cached separately, they bloat the HTML payload, and they make responsive overrides nearly impossible without !important.

A better pattern: set dynamic values as CSS custom properties on the module wrapper, then reference them in your stylesheet:

<!-- In module.html -->
<section class="my-module" style="--module-bg: ; --module-padding: px;">
  ...
</section>

<!-- In your CSS -->
.my-module {
  background-color: var(--module-bg, #ffffff);
  padding: var(--module-padding, 48px) 0;
}

This keeps your HTML lighter, your CSS maintainable, and your responsive breakpoints clean. It also reduces parsing time because the browser handles CSS custom properties more efficiently than scattered inline style declarations.

What You Cannot Fix (And Should Stop Worrying About)

Some things on HubSpot CMS are outside your control. It helps to know what they are so you stop chasing ghosts:

  • HubSpot's tracking and analytics scripts. They load on every page. They are async but still consume resources. You cannot remove or modify them.
  • HubSpot's form JavaScript. If you use HubSpot forms, their rendering JS loads whether the form is above or below the fold. On pages without forms, it still loads if the conversations widget is active.
  • A perfect 100 mobile score. On HubSpot CMS, a mobile PageSpeed score in the 70s to 80s is genuinely good once you account for platform overhead. Chasing 100 on mobile is not realistic on any CMS that includes tracking and marketing tools by default.
  • Server response time. HubSpot controls hosting, CDN, and server configuration. TTFB is generally solid (HubSpot uses a global CDN), but you cannot tune it yourself.

Focus your energy on what you own: images, fonts, your JavaScript, your CSS, your module architecture, and third party scripts.

A Quick Checklist Before You Start

Run your site through PageSpeed Insights right now. Look at the field data section (real users), not just the lab data. Then work through this list:

  1. Is your hero image under 150 KB? Is it preloaded?
  2. Are all below the fold images lazy loaded?
  3. Is your module JavaScript deferred and in the footer?
  4. Are libraries loaded conditionally, only on pages that use them?
  5. Are your fonts preloaded with font-display: swap?
  6. Do all images have explicit width and height attributes?
  7. How many third party scripts are loading? Which ones are actually used?
  8. Are dynamic module styles applied as CSS custom properties or inline?

You will not get a perfect score. Nobody on HubSpot CMS does. But you can get a fast, responsive site that passes Core Web Vitals in the field, and that is what Google actually cares about when it comes to rankings.