Refactoring 10 Years of Legacy

In my time at Agworld, I embarked on a long-lasting project to pay back part of the technical, and design-debt that had built up in its core web product over a decade. The focus was to look at the way we handled styling, with a view to improve performance, legibility, accessibility and overall developer experience.

Agworld takes its technology very seriously, so this was seen as an extremely important project that I was well-suited to tackle. I led this project over the course of 3 cycles lasting 6 weeks each; with the assistance from members of a team dedicated to technical improvements.

Four Bootstrap-sized Problems

The Agworld codebase at the time was a study of its history; filled with interesting artefacts and styling decisions in an environment constantly in flux. Over the years - tastes, personnel, technology and "best practices" evolve, and these were represented prominently, which made it a significantly challenging landscape to work in.

The main problems and pain points that I identified here were:

Results from a CSS audit
Results from a CSS audit

One Bite at a Time

for each desired change, make the change easy (warning: this may be hard), then make the easy change - Kent Beck

A colleague of mine mentioned this quote around the time that this project was kicking off, and it's certainly far easier than it sounds! It was extremely difficult to get a broad understanding of the breadth of the change that I needed to make, so I approached it by dividing work into high-level themes, and then those themes into steps and stages that could be shipped incrementally.

Defer-loading Resources

Initially, identifying key performance budgets and metrics was my top priority for this project. Understanding how we were performing, and targeting changes specifically to improve these scores provided a great baseline for work. I focused primarily on First Contentful Paint (FCP) with this work, with an eye to improving Time to Interactive (TTI)

Certainly, the largest problem here was the size of the payload that needed to be served, but the order that it was loaded was also something that needed to be rectified.

The first change I decided to make here was to make sure that these resources were defer-loaded, or loaded asynchronously, so that it didn't block the critical rendering path.

Screenshots of Lighthouse outputs for different pages
A small, but significant improvement

From the figures above, the measurable FCP score saw improvements of ~50% on some pages, and from not measurable to well below 3s on others - which is a significant improvement, and allowed some of the other more impactful refactoring to take place.

Splitting the Bundle and Removing CSS Dead Wood

Agworld's engineering team employs a high shipping cadence, and has spent significant resources improving this over the time I worked there. At one stage we - as a team - went from shipping a large update every 2-4 months, to shipping several times per day, and dozens of times per week. This exposed an issue with the core product's architecture, where it forced a refreshed asset payload on a large number of those deployments. When the payload is as large as this was, that could be extremely impactful on users - particularly those located in remote areas; so it was extremely important that these assets were cut down to size.

Graph showing size and impact of web resources
Significant, blocking resource payloads

The great majority of this payload was actually unused - meaning it had literally no impact on the final product. On some of the most heavily used pages of the site, over 95% of the served CSS was unused. This was due to the fact that all resources were bundled together, even if they weren't relevant to that particular page. So one page may be using Bootstrap 3, another with Bootstrap 4, and all of it was combined and served together.

The solution I embarked on was to split these resources and only serve what was required for a particular page.

Screenshots of Github comments and PRs showing number of lines removed
Removing and splitting resources

In real-world numbers, the initial size of the bundle was ~430kb. Spltting this up into what was required for different pages meant potentially some only needed to download ~131kb, and others ~162kb. Any reduction here is welcome, but this represented one of the most signifcant, technical savings to the company in the running of the core product. This work was also volatile, so I - along with help from the technical team - put in significant testing time and effort to ensure high quality.

Consolidating layout

The final step in this refactoring project was to consolidate the three, vastly different layouts into one.

Spreadsheet showing an audit of layouts and pages
Auditing layouts in Agworld's web product

This work would unblock further initiatives I would go onto undertake around making Agworld's core product responsive; but also dramatically reduce the cognitive load and complexity facing developers working on new and existing features. A real win for the developer experience at Agworld.

Sandbox Resources

Initially, the first problem that I faced here was determining how to ensure that legacy content and experiences still behaved as desired with a single layout. It was important to ensure that features that people were using out in the field were not disrupted.

The broken layouts along the way
Uh oh! I think I broke something...

I decided to use a sandboxing strategy to isolate resources, layout, and content based on the different layout "contexts" (my words) identified in the audit shown above. Each of these served namespaced layout, resets, and resources required for content within it - effectively shielding it from other, potentially damaging influences.

Small changes in overall design
Small changes in overall design
Subtle changes and improvements in consistency of experience

Completing this meant that legacy content could safely be maintained, regardless of other work that had been undertaken. It's difficult to overstate just how important this is from a maintainability perspective. Being able to utilise the same markup in any context leads to a far more consistent developer experience, which in turn can make maintenance a lot more straightforward.

Improving Experience - Perceived performance

Improving performance metrics is certainly a great way to improve the overall experience that someone has with your product, but there are times when doing this can unintentionally create a poor experience for the person using the product. Deferring the loading of styling assets - in this instance - created quite a significant Flash of Unstyled Content (FOUC). This is where the structure of a page loads in a raw, unstyled format before styling is loaded and applied.

While the product loaded significantly faster than it did before, the overall experience felt worse because of this FOUC; so I created a flexible "loading experience" for developers to opt into and customise to the experience that they wanted to achieve. This could be as simple as a loading spinner, or as complex as an animated skeleton, or loading screen.

Animated image demonstrating the simple loading experience
A simple loading experience

Like all things, making sensible defaults and opt-in experiences is important to improving the overall developer experience.

Clearing the Path Forward

This was one of the most impactful technological projects I undertook during my time at Agworld; working in the context of 10 years of legacy front-end code; and achieving some significant outcomes: