Technical SEO

How We Got a Perfect 100
PageSpeed Score on a Law Firm Website

The exact changes we made to push a law firm website to a perfect 100 across all four Lighthouse categories. Real code, real numbers, no theory.

Reading path

Technical fixes matter most when they support the whole site.

Use technical articles as decision support for crawl cleanup, speed work, schema, and internal linking, then connect them back to the service and audit layer.

11 min read Reading time
2,200 Words
8 FAQs answered
Mar 30, 2026 Last updated

We recently pushed our own site, lawfirmseo.pro, to a perfect 100 across all four Lighthouse categories: Performance, Accessibility, Best Practices, and SEO. Mobile and desktop.

This is not a theoretical guide. These are the exact changes we made, in order, with the reasoning behind each one. The site is built on Astro (static output), uses custom fonts, runs Google Tag Manager, and has animated hero sections with floating cards and SVG chart animations. It is not a blank page.

Here is where we started.

Mobile: 92 Performance. LCP at 3.0s. TBT at 90ms. CLS at 0.029. Desktop: 92 Performance. LCP at 1.3s. TBT at 180ms. CLS at 0.

Here is what we changed.

1. Deferred Google Tag Manager until the browser is idle

This was the single biggest win.

GTM was loading synchronously in the <head>. That meant the browser was downloading, parsing, and executing ~264 KB of JavaScript before the page was usable. Lighthouse flagged ~121 KB of that as unused during initial load.

The standard GTM snippet creates a script element with async=true, which is better than synchronous, but it still fires immediately. The browser starts fetching GTM the moment it hits that line in the <head>, and the resulting JavaScript competes with layout and paint work on the main thread.

We replaced the standard snippet with a deferred version:

window.dataLayer = window.dataLayer || [];
(function () {
  function l() {
    window.dataLayer.push({
      "gtm.start": new Date().getTime(),
      event: "gtm.js",
    });
    var j = document.createElement("script");
    j.async = true;
    j.src = "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX";
    document.head.appendChild(j);
  }
  if ("requestIdleCallback" in window) {
    requestIdleCallback(l, { timeout: 3500 });
  } else {
    setTimeout(l, 2000);
  }
})();

requestIdleCallback tells the browser: load this when you have nothing more important to do. The timeout: 3500 is a ceiling. If the browser never finds idle time within 3.5 seconds, it fires anyway. For browsers that do not support requestIdleCallback, we fall back to a simple 2-second delay.

We also added a preconnect hint for www.googletagmanager.com so the TLS handshake happens early, even though the script itself loads later.

Result: TBT dropped from 90ms to 70ms on mobile. Desktop TBT improved significantly. The “unused JavaScript” diagnostic remained in Lighthouse (it still flags GTM’s tree-shaking potential), but the actual blocking impact on page load was eliminated.

Important note: deferring GTM means analytics events that fire during the first few seconds will not be captured. For most law firm sites this is an acceptable tradeoff. If you rely on tracking the very first interaction within 2 seconds of page load, you will need to weigh that against the performance cost.

2. Added fetchpriority=“high” to the LCP image

Lighthouse identified our nav logo SVG as the Largest Contentful Paint element. It is the first meaningful visual the user sees in the viewport.

The image was already in the HTML (not lazy-loaded, not injected by JavaScript), which is correct. But it was missing fetchpriority="high".

<img
  src="/img/files/logo-white.svg"
  alt="LawFirmSEO.pro"
  width="220"
  height="60"
  fetchpriority="high"
/>

fetchpriority tells the browser’s resource scheduler to prioritize this image over other requests at the same priority level. Without it, the browser treats the logo the same as every other image on the page.

This is a one-line change. It costs nothing. There is no reason not to do it on whatever element Lighthouse identifies as your LCP.

3. Preloaded all font files

We were already preloading two of our four font files (DM Sans regular and DM Serif Display regular). The italic variants for both families were not preloaded, which meant they chained in the network waterfall: the browser discovered them only after parsing the CSS that referenced them.

<link rel="preload" as="font" type="font/woff2" href="/fonts/dm-sans-latin.woff2" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/fonts/dm-sans-italic-latin.woff2" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/fonts/dm-serif-display-latin.woff2" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/fonts/dm-serif-display-italic-latin.woff2" crossorigin />

All four @font-face declarations already used font-display: swap with size-adjusted fallbacks (Georgia for serif, Arial for sans), so there was no visible flash. The preloads just moved the font downloads earlier in the waterfall.

Trade-off: preloading four fonts means four high-priority requests during initial load. If you have fonts that are only used below the fold, do not preload those. Only preload what the first viewport actually needs.

4. Fixed non-composited CSS animations

Lighthouse flagged “3 non-composited animations” on desktop. These were animations using filter and box-shadow properties, which the browser cannot run on the GPU compositor layer by default.

The affected elements:

  • An SVG chart with a filter: drop-shadow() glow animation
  • Chart line paths with stroke-opacity animation
  • A calendar element with box-shadow pulse animation

The fix is will-change, which tells the browser to promote these elements to their own compositor layer:

.hero-chart svg {
  will-change: filter;
}
.hero-chart-line {
  will-change: stroke-dashoffset, stroke-opacity;
}
.hero-chart-fill {
  will-change: opacity;
}
.cal-mock-day.today {
  will-change: box-shadow;
}

will-change is not free. Each promoted element consumes GPU memory. Do not blanket-apply it to everything. Use it only on elements that are actively animating properties the compositor cannot handle natively (anything other than transform and opacity).

For elements that only animate transform and opacity, the browser already composites them. Adding will-change there is redundant.

5. Inlined all CSS to eliminate the render-blocking stylesheet

This was the second-biggest win, and the one that finally pushed us past the threshold.

Astro was generating a single bundled CSS file (~13 KB compressed) and linking it with a standard <link rel="stylesheet">. That is a render-blocking request. The browser cannot paint anything until it downloads and parses that file.

On a fast desktop connection, 13 KB takes maybe 60ms. On mobile with simulated slow 4G, Lighthouse measured 170-860ms of render delay from that single file.

The fix was one line in the Astro config:

export default defineConfig({
  build: {
    inlineStylesheets: "always",
  },
});

This tells Astro to inline all CSS directly into <style> tags in the HTML document instead of generating external files. The browser can now parse styles immediately after receiving the HTML response. No second round-trip.

If you are not using Astro, the equivalent approach is to extract critical CSS (everything needed for the first viewport) and inline it in the <head>, then load the remaining CSS asynchronously:

<link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'" />

The media="print" trick tells the browser the stylesheet is not needed for screen rendering, so it does not block paint. The onload handler switches it back to all once it finishes downloading.

Trade-off: inlining CSS increases HTML document size. Our HTML went from ~15 KB to ~75 KB. For a static site served from a CDN with compression, this is negligible. For a site with many unique stylesheets per page or very large CSS bundles, you may want to inline only the critical portion.

6. Preloaded the LCP image in the document head

In addition to fetchpriority="high" on the <img> tag, we added a preload link in the <head>:

<link rel="preload" as="image" href="/img/files/logo-white.svg" type="image/svg+xml" fetchpriority="high" />

This matters because the browser discovers <link rel="preload"> tags during the initial HTML parse, before it reaches the <img> tag in the body. For the LCP element specifically, you want the browser to start fetching it as early as physically possible.

What the numbers looked like after

Mobile: 100 Performance. FCP under 1s. LCP under 2.5s. TBT under 50ms. CLS 0. Desktop: 100 Performance. FCP under 0.4s. LCP under 0.8s. TBT under 50ms. CLS 0.

All four categories at 100: Performance, Accessibility, Best Practices, SEO.

What we did not do

A few things that were not necessary and would have been over-engineering:

  • We did not remove GTM entirely. We deferred it. Analytics still works.
  • We did not strip animations. We fixed the compositor hints so the browser handles them efficiently.
  • We did not switch to a system font stack. We kept our custom fonts and preloaded them properly.
  • We did not reduce visual complexity. The hero still has animated floating cards, SVG chart animations, and a mesh gradient background. None of that was the problem.
  • We did not use a separate critical CSS extraction tool. Astro’s built-in inlining handled it.

The site looks identical before and after. The difference is invisible to the user. That is exactly the point.

The general pattern

If you are working on a law firm site (or any site) and want to push toward 100, the process is:

  1. Identify the LCP element. Preload it. Add fetchpriority="high". Make sure it is in the HTML, not injected by JavaScript.
  2. Defer third-party scripts. GTM, chat widgets, review widgets, call tracking. None of these need to load before the page is usable. Use requestIdleCallback or interaction-triggered loading.
  3. Eliminate render-blocking CSS. Inline critical styles or inline everything if your CSS is small enough. Every external stylesheet in the <head> is a round-trip the browser has to wait for.
  4. Preload fonts you use above the fold. Use font-display: swap with size-adjusted fallbacks to prevent layout shift during swap.
  5. Fix non-composited animations. If you animate filter, box-shadow, clip-path, or any property other than transform/opacity, add will-change to promote the element to the compositor.

None of these steps require a redesign. None of them require removing features. They are about sequencing: making sure the browser does the right things in the right order, and does not waste time on things the user does not need yet.

A perfect score is achievable on a real, working, visually rich website. It just requires caring about the order in which things happen.

Need a clearer next move?

Get a Free Performance Audit

We'll run a full Lighthouse analysis on your law firm's website, identify the specific bottlenecks, and provide a prioritized fix list with expected impact.

Next steps

Use this topic inside the right part of your growth system.

The strongest next move is usually a technical service review, a deeper implementation guide, or a tool that helps you validate the basics.

Related reads

Other articles firms usually read next.

These are the closest matches by topic, so the next click keeps building useful context instead of sending you sideways.

Frequently asked questions

Technical SEO FAQ

Quick answers to the most common questions about this topic.

01

Is a perfect 100 PageSpeed score realistic for a law firm website?

Yes. We achieved it on lawfirmseo.pro, which runs custom fonts, Google Tag Manager, animated hero sections with floating cards, SVG chart animations, and a mesh gradient background. A perfect score does not require stripping features or simplifying your design. It requires sequencing resources correctly so the browser does the right things in the right order.

02

What was the single biggest PageSpeed improvement?

Deferring Google Tag Manager using requestIdleCallback. GTM was loading synchronously in the head, forcing the browser to download, parse, and execute roughly 264 KB of JavaScript before the page was usable. Deferring it until the browser had idle time dropped Total Blocking Time from 90ms to 70ms on mobile and eliminated the render-blocking impact entirely.

03

Does deferring Google Tag Manager affect analytics accuracy?

Deferring GTM means analytics events that fire during the first 2-3 seconds of page load will not be captured. For most law firm sites, this is an acceptable tradeoff because very few users interact within that window. If your business relies on tracking interactions within the first two seconds, you will need to weigh the analytics gap against the performance cost.

04

What does fetchpriority high do for LCP?

The fetchpriority attribute tells the browser's resource scheduler to prioritize a specific image over other requests at the same priority level. Without it, the browser treats your LCP image the same as every other image on the page. Adding fetchpriority high to your Largest Contentful Paint element is a one-line change that costs nothing and can measurably improve LCP timing.

05

Should I inline all CSS on a law firm website?

If your total CSS is under 50 KB compressed, inlining everything is a good tradeoff. It eliminates the render-blocking round-trip for an external stylesheet. Our CSS was about 13 KB compressed, and inlining it removed 170-860ms of render delay on simulated slow 4G. For sites with very large CSS bundles, inline only the critical styles needed for the first viewport and load the rest asynchronously.

06

Does preloading fonts hurt performance?

Only if you preload fonts that are not used above the fold. Each preloaded font is a high-priority request during initial load. We preload four font files because all four are used in the first viewport. If a font only appears below the fold, do not preload it. Combine preloading with font-display swap and size-adjusted fallback fonts to prevent layout shift during the swap.

07

What is will-change in CSS and when should I use it?

The will-change property tells the browser to promote an element to its own GPU compositor layer. This is necessary when you animate properties like filter, box-shadow, or clip-path that the browser cannot composite natively. Use it only on elements that are actively animating non-composited properties. Do not blanket-apply it, because each promoted element consumes GPU memory.

08

What PageSpeed metrics matter most for law firm SEO?

Largest Contentful Paint and Total Blocking Time have the most weight in the Lighthouse performance score. LCP measures how quickly the main visible content appears. TBT measures how long the main thread is blocked by JavaScript. Fixing these two metrics will produce the largest score improvements. Cumulative Layout Shift matters for user experience but has less weight in the score calculation.

Next step

Want a Perfect PageSpeed Score for Your Law Firm?

We've done it for our own site. Book a free strategy session and we'll audit your current performance, identify the biggest bottlenecks, and show you the exact fixes.

Book my strategy call Free Website Grader
No obligation 100% confidential Custom roadmap included