Site speed

Optimising CSS and JavaScript

Optimising CSS and JavaScript is three jobs, and they are worth doing in this order: delete what is not needed, send later what is not needed immediately, and shrink what remains. On this site, minifying the main stylesheet saved 16.3 KB on the wire, and one analytics file we did not write takes 171.1 KB.

  • Lesson 7 of 10
  • Intermediate
  • Free, no signup
Code delivery sheetin this order

Work in this order

  • weigh every file on the wire, not on disk
  • for each row ask what feature it gives and whether it is needed here
  • move what survives out of the render path with defer or async
  • minify last, and read the number after compression
  • test once after each change, not after ten of them

These eat time and return little

  • minifying a file that should not be on the page at all
  • adding defer to every script in one sweep
  • taking the percentage a tool shows before compression as your yardstick
  • shipping a critical CSS tool output without looking at the page

Last checked: Facts and tool names in this lesson are re-checked against their sources on this date.

Before optimising anything, weigh what is actually there

Most speed tutorials start in the middle: minify, combine, defer. All of that is correct and all of it comes after a question nobody asks, which is what this file is doing here in the first place.

To be able to ask it you first have to see the real weight of each file, not its size on disk. What matters is the size on the wire, the compressed thing that actually reaches the visitor. This command lists every script on a page at that size:

curl -s https://example.com/ \
  | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | sort -u \
  | while read u; do printf '%8s  %s\n' \
      "$(curl -s -o /dev/null -H 'Accept-Encoding: gzip' -w '%{size_download}' "$u")" "$u"; done

There is a trap in that command we walked into ourselves: if an address in the HTML contains an &, it is written as & and that line reports zero bytes. Zero means not measured, not lightweight. Fix that one line by hand or ignore it.

The output of this command on one of our own pages, heaviest first, was: 171.1 KB for the Google analytics file, 29.9 KB for jQuery, 5.9 KB for our own main script, 1.9 KB for a price calculator and under a kilobyte for a captcha script. The Cloudflare captcha script lives on their own domain and came back as zero from the command line, so we left it out of the arithmetic and say plainly here that we do not know its size.

Now add it up. What we wrote is 7.8 KB and what we did not write is 201.5 KB. So 96.3 percent of the JavaScript weight that page sends is somebody else code. No amount of minification moves that ratio.

A tangle of black cables with red scissors, one green coil set aside and one thin blue cable running clean

How much does minifying save? Read the number after compression

Minifying means removing everything from a file the browser does not need: whitespace, comments, long names and, in the better tools, duplicate rules that get merged. It is work you should do and it is free, but the number the plugins show you is the wrong number.

We measured the main stylesheet of this site two ways, once raw and once compressed with gzip, the way it is actually sent. The original is 129.0 KB and compresses to 32.3 KB. The minified version is 79.1 KB and compresses to 16.3 KB.

Now put the two savings side by side. On paper, minifying removed 49.9 KB, close to 39 percent, and that is the figure your tool report shows you. On the wire, the same work removed 16.0 KB. Both numbers are true and only the second one affects speed.

Why does that matter at all? Because if you take the paper number as your yardstick, you prioritise your next decisions wrongly. An optimisation showing a 40 percent saving on paper may be a few kilobytes on the wire, while deleting a small but incompressible script can be worth several times more. The number after compression is the only one a visitor feels.

One thing said less often: a large share of the minification win on this site comes from stripping the comments inside the file. Our main file carries long Persian comments written for humans; gzip compresses them well but does not remove them. If your file has no comments, the gap between compressed and minified will be smaller, and that is normal.

One file, four ways of measuring it

the main stylesheet of this site, measured on 8 September 2026
  1. the original, on disk 129.0 KB with all its comments and whitespace
  2. the minified version, on disk 79.1 KB the very number tool reports show you
  3. the original, compressed on the wire 32.3 KB this is where the real size begins
  4. the minified version, compressed on the wire 16.3 KB the only number a visitor feels

Only the bottom two rows concern the visitor; the top two are sizes on disk. A large share of this reduction comes from stripping comments inside the file, so on a file without comments the gap will be smaller.

defer or async: which one, and where

An ordinary script stops the page from being built while it downloads and runs; how that building proceeds is the subject of how a browser builds a page. Two attributes change that behaviour, and you need to know the difference precisely, because in the wrong place one of them breaks the page.

With defer the browser starts downloading the file immediately but holds its execution until the page has been built, and several scripts carrying that attribute run in the order they appear in the page. With async the download also starts immediately but execution happens the moment the file is ready, wherever the work has got to, and order is guaranteed by nothing.

The practical rule falls out of that difference. Any script that depends on the rest of your code or touches page elements wants defer. Any script that is entirely independent and bothers nobody, such as analytics, takes async. If you are unsure, defer is the safer choice, because its worst case is running slightly later and the worst case of async is an unreproducible error that only happens on a slow connection.

Now the thing tutorials say least and that matters most in practice: where the script sits in the document. If every script is at the bottom of the body, even an attribute-free script blocks very little, because by then the page has been built. On the front page of this site the first script sits at 96.6 percent of the document and there is no external script in the head at all. Three scripts there still carry no attribute and we have not fixed them, because the plugin that inserts them decides for us.

And a warning that breaks more sites than anything else in this job: adding defer to everything in one sweep. There are plugins that do this behind a single switch, and on a site whose code depends on execution order the result is a page that works at random. One at a time, and test once after every change.

defer and async are both right, for two kinds of script

defer

  • download starts at once, execution after the page is built
  • order is preserved, so code depending on other code survives
  • for anything that touches page elements

async

  • download starts at once, execution the moment the file is ready
  • order is guaranteed by nothing, so it breaks dependencies
  • for wholly independent things such as analytics

Neither beats the other. If you are unsure, choose defer: its worst case is running late, and the worst case of async is an error that only shows up on a slow connection.

Critical CSS and inlining: a win that costs something

Every external stylesheet is a render-blocking resource: the browser draws nothing until it has it. The common answer is critical CSS, writing the fragment needed for the first view into the page itself and letting the rest arrive later. We went one step further and inline the whole stylesheet; on the front page of this site there is no link rel=stylesheet tag at all and every rule sits in four style blocks in the head.

That decision has a win and we pay for it, and because almost nobody writes down the price, here is ours. We took the same HTML, replaced the main style block with an external link and compressed both versions. The version actually sent is 41.8 KB and the version with an external sheet is 26.0 KB. So inlining adds 15.8 KB to every HTML response, for every visit, every time.

The break-even is simple arithmetic. A visitor who sees one page and leaves pays nothing extra, because they had to fetch that CSS either way. A visitor who sees five pages fetches the same 15.8 KB four more times, where an external sheet would have been fetched once and come from the browser cache after that. So this choice depends on the shape of your traffic rather than on a general rule: for a site whose visits mostly arrive from search and end on one page, inlining wins.

What separates this from an opinion is that we made it reversible. Both the inline mode and the external mode live in the code and are switched by one constant, precisely so both can be measured and the winner kept. If the shape of the traffic changes, so does the decision.

And a warning about critical CSS tools: their output has to be looked at. These tools decide which rules the first view needs and they get it wrong on pages whose layout differs from the rest. The result is a page that flashes unstyled for a moment, which is worse than a blocking stylesheet.

Put the CSS in the page or in a separate file?

Inside the page

  • no render-blocking resource is left
  • the CSS arrives with the HTML, with no second round trip
  • but the same bytes are sent again on every visit
  • better for a one-page visit arriving from search

In a separate file

  • downloaded once and then served from the browser cache
  • every HTML response gets lighter
  • but until it arrives the browser draws nothing
  • better for a visitor who reads several pages

We chose inline and the price is 15.8 KB on every HTML response. The right choice depends on the shape of your traffic, not on a general rule.

The code you did not write

This is where the lesson takes a position, and it takes it against ourselves: everything the four sections above describe is done to the code you wrote, and on the average site the code you wrote is the minority.

On that same page of ours, jQuery is 29.9 KB. We use not one line of it and we wrote not one line of it; the security plugin that puts up the captcha needs it and WordPress sends it. The analytics file is 171.1 KB and that too belongs to Google, merely hosted on our own server. Those two plus the captcha script come to 201.5 KB, against 7.8 KB of our own code.

Now the point: neither of those can be solved by optimisation. jQuery is already minified and it compresses. So does the analytics file. Both step out of the rendering path with async or defer and both still take the same number of bytes off the visitor connection and the same amount of work off their phone processor. The only decision that changes these numbers is whether you want the feature at all.

We have not made that decision yet and here is the honest reason: the captcha keeps spam out of our forms and analytics tells us which page is working. Both carry a value we still judge to be above their cost. What we have done is contain the cost; jQuery is not on every page, only on pages carrying a form. Blog pages and the lessons in this section do not receive it at all.

So the order of work for you is this. Take the list first. Then for each row ask what feature it provides and whether that feature is needed on this page. Send what survives later, and shrink what still survives after that. Reverse the order and you will spend hours on a few kilobytes and never see the 171. And if your next question is how much JavaScript your site needs at all, the answer is in JavaScript for beginners.

The fast path, with AI

The usual way of finding out which script can be sent later is to change one, look at the site, and if nothing broke move to the next. With three files it works; with twenty it takes days. The fast path is to hand the model the sorting job and not the deciding job. What separates this from asking a normal question: do not give the model a bare list of files, tell it for each file which plugin or feature brings it. A model cannot tell from a filename that a script depends on execution order; from what the feature does, it can make a good guess. What comes out is a prioritised list that you then work through one at a time.

  1. Take the list of scripts on the page with the command from the first section, with sizes on the wire.
  2. Beside each row write which plugin or feature brings it. If you do not know, write that you do not know; that is information too.
  3. Hand the list over with the recipe below and ask for three buckets: removable, deferrable and do-not-touch.
  4. Start at the top of the list and make exactly one change at a time. After each change open the page and look at the browser console. This step cannot be given to a model and cannot be skipped.

Copy-ready recipe

This is the list of scripts on a page of my site. First column the size on the wire in bytes, second the address, third the plugin or feature that brings it.

{paste the list here}

Site type: {for example a WordPress shop | a company site | a blog}
Features actually used on this page: {contact form | slider | map | none}

Sort the list into three buckets and order each bucket heaviest first:
1. Removable: a feature not used on this page.
2. Deferrable: has to stay but is not needed for the first view. For each one write defer or async and one sentence of reasoning.
3. Do not touch: anything depending on execution order or on page elements.

Rules:
1. Work only from this list. If you do not have enough information to decide about a row, put it in bucket three and write what you would have needed.
2. Never put a file you do not recognise in bucket one.
3. If a row comes from a security or payment plugin, bucket three is the default.
4. For every suggestion in bucket two, write what would visibly break if it is wrong. That sentence is exactly what I have to test after the change.
5. Do not invent an estimated saving and do not promise anything about speed.

Give the output as three lists, with no preamble.

Before you trust the output: The model cannot see your site, so anything it puts in the removable bucket is a hypothesis and not a conclusion. Its most dangerous case is a file it does not recognise and guesses at from the name, which is why rule two exists above. The last step cannot be delegated either: after every change, open the page and look at the browser console. The sentence asked for in rule four exists precisely for this, because the worst failures in this job are silent and the page looks fine while one button has stopped working.

AI in this kind of work

Our position here: AI is good at sorting and explaining in this job and bad at deciding. The difference can be stated precisely. Which files can be sent later is a sorting question and a model answers it well. Whether deleting this file breaks something is a question about your site, and the model has not seen your site. The sentence worth keeping is this: the model sorts the list and you make the change.

Tools that actually help

  • Claude Suited to the sorting job above, and especially to working under rules: tell it not to put an unrecognised file in bucket one and it generally does not. Iran is not on the Anthropic supported-countries list, so there is no official signup and no Iranian card is accepted.
  • Gemini Useful for working out what an unfamiliar script belongs to, since it usually knows the names of well-known libraries and plugins. Google own page says the Gemini web app runs in over 230 countries and territories, and Iran is not on that list.
  • ChatGPT Enough for the same sorting and explaining. We make no claim about access from Iran, because the OpenAI supported-countries page, like the rest of that domain, returns 403 to this server.

Where it backfires

Two risks, and the second belongs to this lesson specifically. First, the model invents savings figures. Left unchecked it produces sentences like this change will improve page speed by forty percent, with no measurement behind them, which is why we forbid it explicitly in the recipe above. The second risk is worse because it breaks things quietly: the model writes replacement code. Ask it how to remove jQuery and it will confidently rewrite that code for you in plain JavaScript. That code often works and sometimes misses an edge case that only shows up on mobile or only when the form has an error. If you want to do such a replacement, test it like any new code and not like a harmless optimisation.

Sources: MDN: the script element Anthropic: supported countries Google: where Gemini Apps are available

Where this advice stops

This lesson is about delivering code, not about writing it; whether your code is slow in itself is a different subject and none of this fixes it. It has three other boundaries. First, the numbers on this page belong to one site and will differ on yours, especially the minification share, which depends on how many comments the file carries. Second, the inlining decision depends on the shape of your traffic, and our choice suits a site whose visits are mostly single-page and arrive from search; for a single-page application or a site where users stay for long sessions the answer is the opposite. Third, we have no first-hand experience of very large sites with tens of thousands of pages and several development teams, and we make no claim about them.

From our own work

The most instructive thing we learned about this job came from a measurement on our own site that we did not expect. We have done everything this lesson recommends to our own code: the stylesheet is fully inlined, there is no external stylesheet tag on the page, our own scripts all carry defer, and the first script sits at 96.6 percent of the document. Then we weighed what was really there and found a 171.1 KB analytics file and a 29.9 KB jQuery on the page, neither of which we wrote, against 7.8 KB of our own code. So on the very page where we had done everything right, 96.3 percent of the JavaScript weight was outside our hands. From that day our order of work changed: the weight list first, then a question about each row, and optimisation last.

Real follow-up questions

Should I combine my CSS and JavaScript files?

Less than you think. Combining existed to reduce the number of requests, and on modern connections that cost has fallen a great deal. It carries a real downside instead: one large combined file invalidates the whole browser cache on every small change. If you have to choose between combining and deleting, delete.

Should I install an optimisation plugin or do it by hand?

A plugin is the right choice for minifying and compressing, because that work is mechanical and its mistakes are reversible. But switches that add defer to everything or generate critical CSS should be turned on one at a time, with a look at the site after each. And remember that no plugin decides which feature is unnecessary on this page; that decision is always yours.

Should I remove jQuery from my WordPress site?

First find out what wants it, because it is usually not you. If a plugin depends on it, removing jQuery breaks that plugin and you get a silent bug that may go unnoticed for weeks. The safer move, and usually enough, is to keep it only on the pages that genuinely need it; that is what we did on our site, and blog pages do not receive it at all.