How I Took Netalith.com from 81 to 93 on PageSpeed Insights by Removing the Tailwind CDN
A step-by-step case study: migrating a Tailwind CDN config to tailwind.config.js, the CLI version mismatch that broke production, and the exact fix. Django + Tailwind CSS.
Drake Nguyen
Founder & Research Lead
The Problem: Stuck at 81 on PageSpeed Insights
Netalith.com was scoring 81 for Performance and 96 for Accessibility on the mobile PageSpeed Insights report. The full report referenced throughout this post is here: PageSpeed Insights report for netalith.com.
The report's diagnostics pointed to a render-blocking request chain adding roughly 1,140 milliseconds of delay, caused by two stylesheets loading on every single page: the Tailwind CDN script and a separately built stylesheet, loading side by side and duplicating most of the same CSS.
Step 1: Creating tailwind.config.js and Migrating the CDN Config Into It
Before removing anything, the existing theme configuration had to be moved somewhere that did not depend on the CDN script being present. The original config lived inline, next to the CDN script, and referenced a global tailwind object that only exists because that script creates it:
<script src="https://cdn.tailwindcss.com"></script>
A new file, tailwind.config.js, was created at the project root, next to manage.py. Every value from the block above moved into it, wrapped in module.exports instead of assigned to the global tailwind object, plus a content array telling the CLI where the actual template files live, something the CDN build never needed since it compiled everything live in the browser:
// tailwind.config.js
module.exports = {
content: ["./templates/**/*.html"],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
"50": "#eff6ff",
"100": "#dbeafe",
"200": "#bfdbfe",
"300": "#93c5fd",
"400": "#60a5fa",
"500": "#3b82f6",
"600": "#2563eb",
"700": "#1d4ed8",
"800": "#1e40af",
"900": "#1e3a8a",
"950": "#172554"
},
brand: {
DEFAULT: '#2563eb',
dark: '#1d4ed8',
},
}
},
fontFamily: {
'body': [
'Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui',
'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans',
'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'
],
'sans': [
'Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui',
'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans',
'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'
]
}
}
}
One structural detail matters here: colors stays inside theme.extend, not directly inside theme. Placing custom colors directly under theme replaces Tailwind's entire default palette instead of adding to it, which silently removes every default class like bg-blue-600 from the build. fontFamily stays at the same level it was in the original CDN config, matching what the site already depended on.
Next, a source CSS file was created for the CLI to compile from, following the project's existing static file structure (STATICFILES_DIRS pointed at a public/ folder):
@tailwind base;
@tailwind components;
@tailwind utilities;
And the build was configured to output to public/dist/output.css, matching the path the Django template would load through {% static 'dist/output.css' %}.
Step 2: Removing the CDN Script
With the config migrated, the CDN script tag and its inline config block were deleted from the base template entirely, replaced with a single stylesheet link pointing at the file the CLI would generate:
<link rel=\"stylesheet\" href=\"{% static 'dist/output.css' %}\">
At this point nothing had been built yet. This is the step where the next mistake happened.
Step 3: The Mistake — Downloading the Latest CLI Without Checking the CDN Version
To generate the actual CSS file, the Tailwind CLI needed to be installed. Tailwind ships a standalone executable per platform on its GitHub releases page, so no Node.js install is required on Windows: github.com/tailwindlabs/tailwindcss/releases.
The mistake was downloading the executable from the top of that page, which points to the latest release, without first checking which major version the site was actually running. The CDN script that had been in use up to this point was https://cdn.tailwindcss.com, which serves Tailwind CSS v3. The latest CLI executable downloaded from GitHub, at the time, was Tailwind v4. Two different major versions of the same tool, with different internals for reading configuration and scanning for classes, ended up paired together without that mismatch being checked first.
Step 4: Built, Deployed, and the Layout Broke
The v4 executable ran the build command without any errors and produced an output.css file. Locally, most of the page looked fine, so the change was deployed.
On the live site, a hero section using a custom grid split, lg:grid-cols-[1.2fr_0.8fr], collapsed from two columns into one, and the featured image next to it disappeared. There was no error in the browser console and no failed request. The compiled output.css simply did not contain a CSS rule for that grid class.
This is what a v3/v4 mismatch actually looks like in practice: the v4 CLI did not understand the v3-style tailwind.config.js the same way v3 itself would have, so part of the class-scanning behavior did not carry over correctly, and some classes used in the templates were silently absent from the output. The build never failed loudly. It just shipped an incomplete stylesheet.
Step 5: Downloading the Correct v3 Build and Rebuilding
The fix was to go back to the GitHub releases page and download a specific v3 release instead of the default latest link, for example v3.4.19, matching the major version the CDN script and the existing tailwind.config.js syntax were written for. From that release's Assets list, the correct file for a standard 64-bit Windows machine is tailwindcss-windows-x64.exe, renamed to tailwindcss.exe and placed at the project root, replacing the v4 executable.
Rebuilding with the same command against the same tailwind.config.js, now with a version-matched executable, produced a noticeably different output file, and the previously missing grid class appeared in the compiled CSS. The layout matched the original CDN-rendered version once redeployed.
The Complete Process, Done Correctly
Summarized as the sequence that actually works, without the version mistake in the middle:
- Static file locations. Source file at
public/src/input.css, compiled output atpublic/dist/output.css, matching the project's existingSTATICFILES_DIRSstructure so Django's static file handling did not need to change. - Create tailwind.config.js. Move every value from the CDN's inline
tailwind.configobject intomodule.exports, keeping custom colors insidetheme.extend, and add acontentarray pointing at./templates/**/*.htmlor wherever the project's templates actually live. - Remove the CDN script and its inline config block from the base template, replacing it with a stylesheet link to the compiled output path.
- Check the CDN's version before downloading the CLI. The CDN script's URL and behavior indicate the major version in use; the CLI executable downloaded must match it, not just be whatever release is listed as latest on GitHub.
- Download the version-matched executable from the GitHub releases page for the correct tag, for Windows the
tailwindcss-windows-x64.exeasset, renamed totailwindcss.exe. - Build:
.\\tailwindcss.exe -i public/src/input.css -o public/dist/output.css --minify, or with--watchduring active development. - Verify before deploying. Compare the rendered page against the previous CDN-based version across more than one template, and check that the output file size is consistent with a full build rather than a partial one.
The Results
After the CDN was fully removed and the correctly built stylesheet was deployed, along with adding fetchpriority=\"high\" to the largest contentful paint image and fixing two low-contrast UI elements flagged in the same report, the mobile PageSpeed Insights results were:
- Performance: 81 → 93
- Accessibility: 96 → 100
- Best Practices: 100
- SEO: 100
Largest Contentful Paint improved but remains the largest remaining opportunity, tied mostly to image dimensions rather than render-blocking resources at this point, which is the next optimization pass.
What This Was Really Worth Learning
- When replacing a CDN-based tool with a locally built version, check which major version the CDN is actually serving before downloading anything. The default latest download is not automatically the right one.
- A Tailwind build that completes without errors is not proof the output is correct. Missing classes produce no warning, only a broken layout after deployment.
- Custom colors belong inside theme.extend, not directly inside theme, or the entire default color palette gets silently overwritten.
- Verifying a rebuilt stylesheet against more than one page, before deploying, catches version and content-path mismatches that a single homepage check would miss.