← Back to work

Environmental NGO · performance + accessibility rebuild

Centre for Earthworks

Their site took about eleven seconds to show anything on a mid-range phone — the kind most of their audience is on. I rebuilt it to show up in two.

Role
Build · rebuild
Focus
Performance & accessibility
Year
2026
Stack
Astro · TypeScript
The stakes

A site that couldn’t reach the people it was for.

The Centre for Earthworks is an environmental NGO in Nigeria. Most of the people its site is meant for are on mid-range phones, on slow networks. On exactly those phones, the page took about eleven seconds to show anything at all.

I built the first version of that site earlier in my career. Coming back to it now, the problems were not hard to see — and they were mine to fix.

Before: an empty div until a large bundle downloaded and ran.
After: text, headings and hero image in the first HTML response.
The situation

Nothing appeared until the JavaScript did.

It was a single-page React app. The HTML the server sent was an empty div, and nothing showed until a large JavaScript bundle had downloaded and run. The hero image couldn’t even begin loading: until the app ran, the page didn’t know it existed.

First paint was 11.3 seconds on mobile, 4.6 on desktop. This is where it started.

Performance
46 / 60
Accessibility
90
SEO
83
Decision01of four

Render the page instead of waiting for JavaScript.

I moved the site to Astro. It renders each page to HTML at build time and ships no JavaScript by default; only the interactive parts load any. The text, the headings and the hero image are in the HTML the server sends — so the browser shows them, and fetches them, straight away.

Astro doesn’t make a site fast or accessible on its own. It gets out of the way. The rebuild did the rest.

Decision02of four

Stop shipping the admin editor to visitors.

The old bundle sent everything to everyone. That included the blog editor — rich text, image upload, the whole login stack — which no visitor ever uses. A first-time reader on a phone was downloading the tools the staff use to write posts.

I moved the admin app behind /admin as its own separate bundle. The marketing pages now ship none of it.

454 KB
of editor code removed from every public page.
Decision03of four

Accessibility as the default, not a pass at the end.

Rebuilding from scratch meant I could put the semantics in from the first line: real landmarks, a correct heading order, a menu, dropdown and modal you can work entirely from the keyboard, reduced motion honored.

A keyboard pass turned up bugs a score of 90 never flagged — dropdown links you could click with a mouse but couldn’t reach with the keyboard, among others.

Four icons used the wrong attribute name, so they had no alt text at all — and the build passed without a word. That held Accessibility and SEO under 100; a one-line fix took both to 100.

A clean build and a high score can still ship something broken for someone using a keyboard.

Decision04of four

Make the subscribe form actually work.

The original subscribe form did nothing. It caught the click and stopped the page from reloading, and that was all.

The backend had no subscription endpoint, so I wired the form to a third-party service as a working stand-in. It confirms in place without reloading the page, and it remembers you if you come back.

You’re subscribed.Confirmed in place — no page reload.
Results

Eleven seconds became two.

First paint on mobile went from about eleven seconds to about two. The JavaScript that used to block that first paint no longer does, and the images came down with it.

11.3s~2s
First paint on mobile.
19 MB2.2 MB
Total image weight.
4686
Mobile performance score.
Performance
Mobile
Before46
After86
Accessibility
Mobile
Before90
After100
Best Practices
Mobile
Before77
After100
SEO
Mobile
Before83
After100
Lighthouse — full scores, mobile / desktop
MetricBefore · mobileBefore · desktopAfter · mobileAfter · desktop
Performance46608699
Accessibility9090100100
Best Practices7777100100
SEO8383100100
What’s left

The gaps, and two tradeoffs.

Mobile is 86, not in the 90s. The gap is almost entirely one animation doing too much work while the page loads. The fix is straightforward and the gain is small, so I left it.

  • 01The blog is fetched in the browser rather than built into the HTML. That weakens its SEO — which the organization didn’t need — and in return new posts appear the moment they’re published, with no rebuild.
  • 02The subscribe form is a third-party stand-in rather than the org’s own backend, until there’s an endpoint to point it at.
Closing

Two seconds instead of eleven is a number. What it means is that someone on a slow phone, on a slow network, can read the site before they give up on it. That’s who the work was for.

This was an NGO, not a devtools company — but the work doesn’t change much: find out who has to use the thing, and make sure it works for them.