062 —Frontend
Auditing checkout JavaScript: the 15-minute network-tab pass
Friday afternoon, you open DevTools on a checkout that nobody has touched in two years and find 38 third-party scripts. Here is the fifteen-minute pass that tells you which ones can go.
It's 15:40 on a Friday and a Magento 1 checkout takes 6.2 seconds to first paint. The shop owner emailed on Wednesday. You open DevTools, switch to the Network tab, filter to JS, and count 38 third-party scripts firing before the pay button renders. Roughly half of them you can't identify on sight.
This is the audit pass for that moment. Fifteen minutes from cold start to a written list of third-party JavaScript that can leave the page today, without bringing in a build pipeline, a heatmap vendor, or a Lighthouse report.
One reload, one HAR file
Open the checkout in an incognito window with cache disabled (DevTools open, Network tab, "Disable cache" checked). Filter the requests panel to JS using the type selector. Hard reload. The point of incognito is to strip out logged-in admin state and any browser extensions that would otherwise pollute the trace.
Right-click anywhere in the requests panel and choose Save all as HAR with content. This is your audit corpus. You will refer back to it three or four times in the next ten minutes, and you do not want to keep refreshing a live checkout while the marketing team is watching the realtime dashboard.
jq '.log.entries[]
| select(.response.content.mimeType | test("javascript"))
| .request.url' checkout.har | sort -uThat gives you a deduplicated list of every JS URL on the page. On a representative checkout, expect somewhere between 25 and 80 lines.
Sort by initiator, not by URL
The URL column is misleading. A request from analytics.tiktok.com tells you it is the TikTok pixel, but it does not tell you who put it there. The Initiator column does. Click the column header to add it if it isn't visible.
What you are looking for is chains: a script loaded by another script loaded by another script. In nine cases out of ten on a legacy checkout, the root of the chain is one of three places:
- The theme's
functions.phpor equivalent (WooCommerce hooks, Magento layout XML). - Google Tag Manager or a similar container snippet pasted into
<head>five years ago. - A plugin's settings page, where a previous developer pasted a tracking snippet into a "custom HTML" field.
Initiator chains in DevTools show you the call stack. A script appearing with gtm.js at the top of its initiator is almost certainly being injected by a tag in your container, even if you do not remember adding it. Open the container in a separate tab and grep the published version for the script's domain. You will find tags whose triggers reference campaigns that ended in 2022.
Three patterns that survive every redesign
The orphan tag manager tag
Someone created a "Marketing Pixel: Black Friday 2023" tag with an All Pages trigger. The campaign ended sixteen months ago. The tag is still firing on every checkout, loading 41 KB of script from a domain that returns 200 because the vendor still bills the long-dormant account.
The plugin you replaced but did not fully uninstall
WooCommerce ships a graveyard of these. A previous developer activated a coupon plugin, decided it was not a fit, and deactivated it. The plugin's tracking call is still listed in wp_options because the uninstall hook never ran. The next theme update wrote a transient that includes the old call. Now it is part of every checkout render.
A quick check with the MySQL editor:
SELECT option_name, LEFT(option_value, 200)
FROM wp_options
WHERE option_value LIKE '%hotjar%'
OR option_value LIKE '%fbq(%'
OR option_value LIKE '%gtag(%';On a fifteen-year-old WordPress install, this query returns four to ten rows. Most are dormant.
The "we'll remove it after the test" script
A consultant added a session-replay script for two weeks of UX research in 2024. They removed the snippet from the theme. They did not remove the entry from the WPRocket excludes list, which is still telling the page to preload the vendor's CDN. That handshake costs you 80ms on every checkout in three countries.
Keep, defer, delete
For every script on the list, write one of three letters next to it: K, D, X.
- K (keep): the script is genuinely needed for the checkout (payment provider SDK, fraud screen, consent banner). Leave it alone, but check whether it can move from
<head>to just before</body>. - D (defer): wanted by marketing, not required for the page to function. Add
deferorasync, or move it behind the consent gate where it should have been since GDPR. The MDN reference on script loading is the one-page version of the trade-offs. - X (delete): nobody owns it, nobody reads its data. Remove it from the source, then add a defensive Content-Security-Policy entry to keep it from coming back via a plugin update.
For the X bucket, a complementary .htaccess rule keeps any plugin that tries to silently reintroduce the same domain visible:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://js.stripe.com https://www.googletagmanager.com; report-uri /csp-report.php"
</IfModule>This is the part most audits skip. Removing the script today does not stop the marketing team from re-adding it via the GTM web UI next week. The CSP makes that addition visible: the script gets blocked at the browser, the report endpoint logs the attempt, you have a conversation on Monday.
A second pass to confirm the wins
Hard reload, re-export the HAR, run the jq command again. The line count should drop. On a representative legacy site checkout audited last month, the count went from 53 JS requests to 31, and first paint moved from 6.2s to 3.4s. The headline number is nice. The list of removals, signed off in writing, is what makes the change survive the next theme update.
The same pass, on the same kind of site, every week
When we built Pier we kept watching this exact audit unfold on customer sites. The MySQL grep for dormant tracker strings, the GTM container archaeology, the version history entry pinned to "removed Hotjar 2024-09-23, marketing confirmed no active account." The way we ended up handling it was to keep the receipts inside the same chat surface where the edits happen, so the audit and the diff live in one place.
If you only have ten minutes today, export the HAR from one checkout page and grep it for three string fragments: hotjar, tiktok, and fbq(. Anything that hits is a candidate. The rest of the audit can wait for Monday.
— Questions —
Can I run this audit without keeping DevTools open the whole time?
Yes. Export a HAR file from any browser that supports the format, then run jq filters offline. The HAR is the audit corpus and survives reboots, so the live checkout only takes the load once.
What if the audit finds a tracker the marketing team still wants?
Move it to defer, async, or behind the consent banner. The point of the pass is removing weight from the critical render path, not removing every script the business has a use for.
How often should this audit run on a production checkout?
After every theme update, plugin install, or marketing campaign launch. Pin a recurring calendar block once a month as a backstop for the ones that slip through without a deploy.
Will a Content-Security-Policy header break legitimate scripts?
Only the ones you did not allowlist. Roll the policy out in report-only mode first, watch the CSP report endpoint for a week, then switch to enforcing mode once the noise has settled.