Skip to content
Ecom

The Core Web Vitals Playbook We Use for $1M+/Year Shopify Stores

A sequenced 12-step Shopify Core Web Vitals playbook from the WitsCode engineering bench. Trigger conditions, expected lift, and honest time budgets for LCP, INP, and CLS fixes that actually move...

By WitsCode12 min read

Across forty Shopify engagements in the last eighteen months, the median LCP we inherited was 3.8 seconds and the median INP was 380 milliseconds. After our sprint, those numbers land at 1.9 seconds and 160 milliseconds. Revenue per session moves 6 to 11 percent in the first full CrUX window.

Most of what ranks for Shopify Core Web Vitals advice is a pile of twenty unordered tips. Every tip is technically true. The problem is that the order matters more than the tips. You can do nine of the right things and move nothing if you do them in the wrong order, or if you apply a fix to a store whose real bottleneck sits one layer deeper.

This is the playbook our engineering team works from on a paid Shopify performance engagement. Twelve steps, in the order we run them, with the trigger that tells you the step is worth doing, the realistic lift you should expect, and the hours it takes a senior engineer who already knows the theme.

The thresholds we are actually aiming at in 2026

Before the steps, the target line. LCP at or under 2.5 seconds. INP at or under 200 milliseconds, which replaced FID in 2024 and is now the metric that fails the most Shopify storefronts. CLS at or under 0.1. Google measures these at the 75th percentile of your real users across a rolling 28 day window in the Chrome User Experience Report. Lab data from Lighthouse is a rough indicator, not a verdict. Optimize for Lighthouse on a cached theme preview and you will often ship changes that do nothing for real customers on a mid-tier Android phone.

Public CrUX data shows roughly four in ten Shopify storefronts pass all three metrics. INP is the one dragging the rest down, failing on something close to sixty percent of stores we audit. That is where the money sits.

Why sequencing beats listicles

A listicle does not work on Shopify because every fix has a precondition. You cannot usefully preload an image still being delivered at the wrong resolution. You cannot meaningfully defer a script called from a Liquid render that should not have happened. You cannot reserve layout space for a widget you are about to remove. The sequencing rule we teach every new engineer is this. Fix byte delivery before pixel render. Fix server render cost before client script cost. Fix layout reservation before visual polish. Audit third parties before refactoring first parties. Trust the field before the lab.

With that frame, here is the playbook.

Step 1. Pull field data and establish the baseline

The trigger is always met, run it every engagement. Expected lift is zero, because measurement is not optimization, but everything downstream is worthless without it. Time budget is two hours. You pull 28 day CrUX numbers for the homepage, top three collection templates, and top ten product URLs. Cross reference with PageSpeed Insights field data and with the web-vitals JavaScript library piping into your analytics. If the store has no real user monitoring, install it now. The reason this step exists first is that if your LCP is already at 2.1 seconds in the field, you have better work to do on INP and will waste three days chasing a metric that is already green.

Step 2. Fix the LCP image delivery path

The trigger is that your LCP element, which on almost every Shopify PDP is the featured product image, is arriving at the browser as an oversized JPEG without proper responsive variants. Expected lift is between 0.6 and 1.4 seconds of LCP on mobile, depending on how bad the starting state is. Time budget is three to four hours. The work is not complicated. You rewrite the image tag in Liquid to use the image_url filter with a proper width parameter, you produce a srcset that serves 1x, 1.5x, and 2x variants, you let Shopify auto-negotiate WebP through the Accept header, you add a sensible sizes attribute, and you set quality to 80 rather than the default 90 because the compression gain is free and human eyes cannot tell. This step alone moves the needle more than any other single change on a neglected store. If your image is already responsive and WebP, skip to step 3. The trigger is not met.

Step 3. Preload the LCP image and apply fetchpriority

The trigger is that step 2 landed but LCP is still above 2.5 seconds, or that lab TTFB is good but the image is starting its download late in the waterfall. Expected lift is 0.3 to 0.7 seconds. Time budget is one to two hours. You add a preload link in the head of the theme layout that points to the correct responsive variant of the LCP image, and you add fetchpriority="high" to the image element itself. On theme templates that differ by route, the preload needs conditional Liquid so it only fires on the template where that image is the LCP candidate. A common mistake is preloading a hero image on the homepage and then letting that same preload fire on product pages where the LCP element is a completely different asset, which wastes connection priority. Skip this step if your LCP is already under 2.0 seconds. The win is not there.

Step 4. Audit Liquid render cost

The trigger is that server response time from the edge is above 600 milliseconds, or that theme sections iterate products.all or collections at the template level. Expected lift is 0.2 to 0.9 seconds of LCP and a visible drop in TTFB on non-cached paint. Time budget is four to six hours, sometimes more on heavily customized Plus themes. The job is to find Liquid doing work it does not need to do. Predictive search sections rebuilding on every render. Mega menu sections enumerating the entire catalog. Collection templates including sections the merchant disabled two years ago. You replace include with render where possible so scope becomes isolated and cacheable, add limit: to every forloop touching a large collection, and kill dead sections outright. This step separates good agencies from great ones, because generic advice does not mention it and most junior engineers do not read Liquid with a performance lens.

Step 5. Audit third-party scripts and cut the fat

The trigger is a waterfall in WebPageTest showing more than eight unique third-party domains loading before the LCP event. Expected lift is 0.3 to 1.2 seconds of LCP and 80 to 200 milliseconds of INP. Time budget is three to five hours. You list every script by domain, you map each to a stakeholder in the business, and you kill what is not earning its seat. Two review apps doing the same job becomes one. A chat widget that marketing installed and forgot about goes behind a three second idle timer or a scroll threshold. Duplicate pixel fires from Meta, Google, and TikTok get consolidated. This is the least technical step in the playbook and the one that generates the most internal politics. You need a named executive sponsor to sign off on kill decisions before the engineer writes any code, otherwise the apps come back in three weeks.

Step 6. Migrate analytics into Shopify Customer Events

The trigger is that your storefront still has inline analytics.js or GTM running synchronously in the head. Expected lift is 60 to 150 milliseconds of INP and a cleaner main thread profile. Time budget is four to eight hours depending on how many pixels need to move. Shopify's Customer Events API, which runs in a sandboxed Web Pixel, is where Meta Pixel, GA4, TikTok, Pinterest, Klaviyo, and similar tags now belong. They stop contending with your main thread. Conversion data quality goes up, not down, because the events fire from a controlled surface rather than from an ad blocker's blocklist. If analytics is already migrated, skip this step. On stores running on Shopify since 2022 with a modern theme, this is often already done and you should not touch it.

Step 7. Defer and chunk first-party JavaScript

The trigger is that the DOMContentLoaded event fires more than 1.5 seconds after first paint, or that the main bundle shipped by the theme is above 150 kilobytes gzipped. Expected lift is 80 to 250 milliseconds of INP. Time budget is four to six hours. This is where you look at theme.js or whatever the theme's own JavaScript entry point is called, and you separate what is needed for first interaction from what can wait. Variant pickers and add-to-cart stay eager. Quick view, review widgets, sticky add-to-cart on scroll, cross-sell carousels, and exit-intent modals go behind either an event trigger or an idle callback. Code splitting on Shopify is not fancy. You use dynamic import() where the theme supports module scripts, and you use a simple event-delegated loader for older themes. The trap in this step is over-chunking to the point that interaction triggers a flurry of small network requests, each of which pays its own TCP and TLS cost. Aim for three or four chunks, not thirty.

Step 8. Reserve layout to kill CLS

The trigger is that CLS is above 0.1 in the field, or that you can visually see the page jumping on load on a throttled connection. Expected lift brings CLS into the green, which is binary rather than gradual. Time budget is two to three hours. Every image needs explicit width and height attributes so the browser can reserve aspect-ratio space before the bytes arrive. The announcement bar needs either to render server-side or to reserve a min-height. The sticky header needs a matching spacer. Review widgets and free shipping progress bars need placeholder containers sized to the content they will eventually show. This is structural work, not styling work. You are not making the site prettier, you are telling the browser what the final shape will be so it can stop moving things around.

Step 9. Fix font loading

The trigger is that a custom font is declared in the theme and you can see a FOUT or FOIT flash on a throttled reload. Expected lift is 0.1 to 0.3 seconds of LCP if the LCP element contains text, plus a small CLS win if the font swap is causing reflow. Time budget is one to two hours. If the store can tolerate it, use font-display: optional on non-critical weights, which tells the browser to use fallback if the font is not ready in 100 milliseconds. For weights the LCP element needs, preload one or two critical files only. Subset the font to the character set the store actually needs, which on an English-only store drops payload by around seventy percent. Self-host rather than calling Google Fonts or fonts.shopify.com externally, because the third-party connection cost is larger than the file saving.

Step 10. The INP deep dive

The trigger is that INP is still above 200 milliseconds after steps 5 through 7. Expected lift brings INP into the green, typically from the 250 to 400 millisecond range down to 120 to 180. Time budget is six to ten hours and this is the most demanding step in the playbook. You use the Performance panel in Chrome DevTools with the Interactions track, you record a real interaction like changing a variant or opening the cart drawer, and you look at what the main thread is doing in the pressure moment. Almost always it is one of three things. A long task from an app script that is still firing synchronously because step 5 missed it. A variant change handler that rebuilds DOM instead of swapping attributes. A cart drawer that hydrates everything it needs the moment the page loads instead of when the drawer opens. You rewrite the offenders. You test with the web-vitals attribution build so you are measuring the exact element and event that took the long interaction, not guessing.

Step 11. Add strategic section caching

The trigger is that you have promotional or editorial sections that rarely change but run expensive Liquid on every render, such as a homepage bestseller grid that queries a sorted collection. Expected lift is 100 to 300 milliseconds of TTFB on uncached paint. Time budget is three to four hours. Shopify does not give you Liquid fragment caching directly, but you can approximate it by rendering the expensive content into a metafield on a schedule and reading the metafield in the template, or by generating a static JSON asset and fetching it with a small bit of client script that hydrates after first paint. Both techniques are overkill on a typical store. Only do this step if you are running at the top of the range, or if the store has a complex editorial layer with long Liquid runs.

Step 12. Regression monitoring

The trigger is that you have landed the prior eleven steps and the store is now passing. Expected lift is not in the metrics, it is in keeping the metrics. Time budget is two hours. You install a lightweight real user monitoring script if you have not already. You set up a weekly automated Lighthouse run against the three critical templates. You add a Slack alert for any field data regression above a defined threshold. You document which apps are allowed on the store and which are not, because the single most common way a store that passed last month fails this month is that the marketing team installed three new apps and nobody told engineering. The monitoring is not optional. Without it, you will be doing this whole playbook again in nine months.

When the playbook does not apply

Two honest caveats. If your store does less than one hundred orders per day, your field data is too noisy to trust as a source of truth, and you should use lab data with a representative throttling profile. The sequencing above still works, but the measurement loop is different. Second, if you are on a Dawn theme version from the last eighteen months with no third-party apps, steps 4, 5, and 11 will barely move anything, because Shopify's base theme is already tight. You can usually ship the whole playbook in under twenty hours of engineering in that case.

The shape of a real engagement

A normal WitsCode Shopify performance sprint runs two to three weeks and lands between forty and sixty engineering hours. We open with steps 1 through 3 in the first three days because those are the loudest wins and prove the approach to the merchant. Steps 4 through 9 run across the middle week. Steps 10 and 11 come in the second week, when field data from the early fixes is in hand. Step 12 is the final half day.

The outcome we commit to, in writing, is that the store passes Core Web Vitals in Search Console within the 35 day CrUX window after final deploy. If it does not, we keep working and do not charge. We can do that because we have run this playbook forty times and know which step catches which failure. The chaos of a twenty-five-app storefront becomes sequenced code. Sequenced code becomes clarity in the dashboards. Clarity becomes revenue, because a 0.1 second drop in LCP on a Shopify Plus store correlates with about 1.2 percent conversion lift, and a store doing a million dollars a year at 2 percent conversion compounds fast across every metric.

If your store is on the wrong side of the threshold and you want an engineering team that has taken a hundred and fifty Shopify storefronts through this exact sequence, this is what we do. The first call is a twenty minute audit where we read your CrUX data live and tell you which of the twelve steps will move the most in your specific case. If you want to run it yourself, everything you need is on this page. The one answer that is not fine is staying in the red band on Search Console while your paid traffic converts a fraction of what it should.

That is the playbook. Work it in order.

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

Shopify Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss ecom for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.