Refactoring CSS: Strategy, Regression Testing And Maintenance (Part 2)

In Part 1, we’ve covered the side effects of low-quality CSS codebase on end-users, development teams, and management. Maintaining, extending, and working with the low-quality codebase is difficult and often requires additional time and resources. Before bringing up the refactoring proposal to the management and stakeholders, it can be useful to back up the suggestion with some hard data about the codebase health — not only to convince the management department, but also have a measurable goal for the refactoring process.

Let’s assume that management has approved the CSS refactoring project. The development team has analyzed and pinpointed the weaknesses in the CSS codebase and has set target goals for the refactor (file size, specificity, color definitions, and so on). In this article, we’ll take a deep dive into the refactoring process itself, and cover incremental refactoring strategy, visual regression testing, and maintaining the refactored codebase.

Preparation And Planning

Before starting the refactoring process, the team needs to go over the codebase issues and CSS health audit data (CSS file size, selector complexity, duplicated properties, and declarations, etc.) and discuss individual issues about how to approach them and what challenges to expect. These issues and challenges are specific to the codebase and can make the refactoring process or testing difficult. As concluded in the previous article, it’s important to establish internal rules and codebase standards and keep them documented to make sure that the team is on the same page and has a more unified and standardized approach to refactoring.

The team also needs to outline the individual refactoring tasks and set the deadlines for completing the refactoring project, taking into account current tasks and making sure that refactoring project doesn’t prevent the team from addressing urgent tasks or working on planned features. Before estimating the time duration and workload of the refactoring project, the team needs to consult with the management about the short-term plans and adjust their estimates and workload based on the planned features and regular maintenance procedures.

Unlike regular features and bug fixes, the refactoring process yields little to no visible and measurable changes on the front end, and management cannot keep track of the progress on their own. It’s important to establish transparent communication to keep the management and other project stakeholders updated on the refactoring progress and results. Online collaborative workspace tools like Miro or MURAL can also be used for effective communication and collaboration between the team members and management, as well as a quick and simple task management tool.

Christoph Reinartz pointed out the importance of transparency and clear communication while the team at trivago was working on the CSS refactoring project.

“Communication and clearly making the progress and any upcoming issues visible to the whole company were our only weapon. We decided to build up a very simple Kanban board, established a project stand-up and a project Slack channel, and kept management and the company up-to-date via our internal social cast network.”

The most crucial element of planning the refactoring process is to keep the CSS refactoring task scope as small as possible. This makes the tasks more manageable, and easier to test and integrate.

Harry Roberts refers to these tasks as “refactoring tunnels”. For example, refactoring the entire codebase to follow the BEM methodology all at once can yield a massive improvement to the codebase and the development process. This might seem like a simple search-and-replace task at first. However, this task affects all elements on every page (high scope) and the team cannot “see the light at the end of the tunnel” right away; a lot of things can break in the process and unexpected issues can slow down the progress and no one can tell when the task is going to be finished. The team can spend days or weeks working on it and risk hitting a wall, accumulate additional technical debt, or making the codebase even less healthy. The team ends up either giving up on the task of starting over, wasting time and resources in the process.

By contrast, improving just the navigation component CSS is a smaller scope task and is much more manageable and doable. It is also easier to test and verify. This task can be done in a few days. Even with potential issues and challenges that slow down the task, there is a high chance of success. The team can always “see the end of the tunnel” while they’re working on the task because they know the task will be completed once the component has been refactored and all issues related to the component have been fixed.

Finally, the team needs to agree on the refactoring strategy and regression testing method. This is where the refactoring process gets challenging — refactoring should be as streamlined as possible and shouldn’t introduce any regressions or bugs.

Let’s dive into one of the most effective CSS refactoring strategies and see how we can use it to improve the codebase quickly and effectively.

Incremental Refactoring Strategy

Refactoring is a challenging process that is much more complex than simply deleting the legacy code and replacing it with the refactored code. There is also the matter of integrating the refactored codebase with the legacy codebase and avoiding regressions, accidental code deletions, preventing stylesheet conflicts, etc. To avoid these issues, I would recommend using an incremental (or granular) refactoring strategy.

In my opinion, this is one of the safest, most logical, and most recommended CSS refactoring strategies I’ve come across so far. Harry Roberts has outlined this strategy in 2017. and it has been my personal go-to CSS refactoring strategy since I first heard about it.

Let’s go over this strategy step by step.

Step 1: Pick A Component And Develop It In Isolation

This strategy relies on individual tasks having low scope, meaning that we should refactor the project component by component. It’s recommended to start with low-scope tasks (individual components) and then move onto higher-scoped tasks (global styles).

Depending on the project structure and CSS selectors, individual component styles consist of a combination of component (class) styles and global (wide-ranging element) styles. Both component styles and global styles can be the source of the codebase issues and might need to be refactored.

Let’s take a look at the more common CSS codebase issues which can affect a single component. Component (class) selectors might be too complex, difficult to reuse, or can have high specificity and enforce the specific markup or structure. Global (element) selectors might be greedy and leak unwanted styles into multiple components which need to be undone with high-specificity component selectors.

After choosing a component to refactor (a lower-scoped task), we need to develop it in an isolated environment away from the legacy code, its weaknesses, and conflicting selectors. This is also a good opportunity to improve the HTML markup, remove unnecessary nestings, use better CSS class names, use ARIA attributes, etc.

You don’t have to go out of your way to set up a whole build system for this, you can even use CodePen to rebuild the individual components. To avoid conflicts with the legacy class names and to separate the refactored code from the legacy code more clearly, we’ll use an rf- prefix on CSS class name selectors.

Step 2: Merge With The Legacy Codebase And Fix Bugs

Once we’ve finished rebuilding the component in an isolated environment, we need to replace the legacy HTML markup with refactored markup (new HTML structure, class names, attributes, etc.) and add the refactored component CSS alongside the legacy CSS.

We don’t want to act too hastily and remove legacy styles right away. By making too many changes simultaneously, we’ll lose track of the issues that might happen due to the conflicting codebases and multiple changes. For now, let’s replace the markup and add refactored CSS to the existing codebase and see what happens. Keep in mind that refactored CSS should have the .rf- prefix in their class names to prevent conflicts with the legacy codebase.

Legacy component CSS and global styles can cause unexpected side-effects and leak unwanted styles into the refactored component. Refactored codebase might be missing the faulty CSS which was required to undo these side-effects. Due to those styles having a wider reach and possibly affecting other components, we cannot simply edit the problematic code directly. We need to use a different approach to tackle these conflicts.

We need to create a separate CSS file, which we can name overrides.css or defend.css which will contain hacky, high-specificity code that combats the unwanted leaked styles from the legacy codebase.

overrides.css which will contain high-specificity selectors which make sure that the refactored codebase works with the legacy codebase. This is only a temporary file and it will be removed once the legacy code is deleted. For now, add the high-specificity style overrides to unset the styles applied by legacy styles and test if everything is working as expected.

If you notice any issues, check if the refactored component is missing any styles by going back to the isolated environment or if any other styles are leaking into the component and need to be overridden. If the component looks and works as expected after adding these overrides, remove the legacy code for the refactored component and check if any issues happen. Remove related hacky code from overrides.css and test again.

Depending on the case, you probably won’t be able to remove every override right away. For example, if the issue lies within a global element selector which leaks styles into other components that also need to be refactored. For those cases, we won’t risk expanding the scope of the task and the pull request but rather wait until all components have been refactored and tackle the high-scope tasks after we’ve removed the same style dependency from all other components.

In a way, you can treat the overrides.css file as your makeshift TODO list for refactoring greedy and wide-reaching element selectors. You should also consider updating the task board to include the newly uncovered issues. Make sure to add useful comments in the overrides.css file so other team members are on the same page and instantly know why the override has been applied and in response to which selector.

/* overrides.css */
/* Resets dimensions enforced by “.sidebar > div” in “sidebar.css” */
.sidebar > .card {
min-width: 0;
}

/* Resets font size enforced by “.hero-container” in “hero.css”*/
.card {
font-size: 18px;
}

Step 3: Test, Merge And Repeat

Once the refactored component has been successfully integrated with the legacy codebase, create a Pull Request and run an automated visual regression test to catch any issues that might have gone unnoticed and fix them before merging them into one of the main git branches. Visual regression testing can be treated as the last line of defense before merging the individual pull requests. We’ll cover visual regression testing in more detail in one of the upcoming sections of this article.

Now rinse and repeat these three steps until the codebase has been refactored and overrides.css is empty and can be safely removed.

Step 4: Moving From Components To Global Styles

Let’s assume that we have refactored all individual low-scoped components and all that is left in the overrides.css file are related to global wide-reaching element selectors. This is a very realistic case, speaking from the experience, as many CSS issues are caused by wide-reaching element selectors leaking styles into multiple components.

By tackling the individual components first and shielding them from the global CSS side-effects using overrides.css file, we’ve made these higher-scoped tasks much more manageable and less risky to do. We can move onto refactoring global CSS styles more safely than before and remove duplicated styles from the individual components and replacing them with general element styles and utilities — buttons, links, images, containers, inputs, grids, and so on. By doing so, we’re going to incrementally remove the code from our makeshift TODO overrides.css file and duplicated code repeated in multiple components.

Let’s apply the same three steps of the incremental refactoring strategy, starting by developing and testing the styles in isolation.

Next, we need to add the refactored global styles to the codebase. We might encounter the same issues when merging the two codebases and we can add the necessary overrides in the overrides.css. However, this time, we can expect that as we are gradually removing legacy styles, we will also be able to gradually remove overrides that we’ve introduced to combat those unwanted side-effects.

The downside of developing components in isolation can be found in element styles that are shared between multiple components — style guide elements like buttons, inputs, headings, and so on. When developing these in isolation from the legacy codebase, we don’t have access to the legacy style guide. Additionally, we don’t want to create those dependencies between the legacy codebase and refactored codebase.

That is why it’s easier to remove the duplicated code blocks and move these styles into separate, more general style guide components and selectors later on. It allows us to address these changes right at the end and with the lower risk as we are working with a much healthier and consistent codebase, instead of the messy, inconsistent, and buggy legacy codebase. Of course, any unintended side-effects and bugs can still happen and these should be caught with the visual regression testing tools which we’ll cover in one of the following sections of the article.

When the codebase has been completely refactored and we’ve removed all makeshift TODO items from the overrides.css file, we can safely remove it and we are left with the refactored and improved CSS codebase.

Incremental CSS Refactoring Example

Let’s use the incremental refactoring strategy to refactor a simple page that consists of a title element and two card components in a grid component. Each card element consists of an image, title, subtitle, description, and a button and is placed in a 2-column grid with horizontal and vertical spacing.

Depending on the project, testing tools don’t need to be complex or sophisticated to be effective. While working on refactoring the Sundance Institute’s CSS codebase, the development team used a simple static style guide page generated by Jekyll to test the refactored components.

“One unintended consequence of executing the refactor in abstraction on a Jekyll instance was that we could now publish it to Github pages as a living style guide. This has become an invaluable resource for our dev team and for external vendors to reference.”

Once the CSS refactor tasks have been completed and the refactored code is ready for production, the team can also consider doing an A/B test to check the effect of the refactored codebase on users. For example, if the goal of the refactoring process was to reduce the overall CSS file size, the A/B test can potentially yield significant improvements for mobile users, and these results can also be beneficial to project stakeholders and management. That’s exactly how the team at Trivago approached the deployment of their large-scale refactoring project.

“(…) we were able to release the technical migration as an A/B Test. We tested the migration for one week, with positive results on mobile devices where mobile-first paid out and accepted the migration after only four weeks.”

Keeping Track Of Refactoring Progress

Kanban board, GitHub issues, GitHub project board, and standard project management tools can do a great job of keeping track of the refactoring progress. However, depending on the tools and how the project is organized, it may be difficult to estimate the progress on a per-page basis or do a quick check on which components need to be refactored.

This is where our .rf-prefixed CSS classes come in. Harry Roberts has talked about the benefits of using the prefix in detail. The bottom line is — not only do these classes allow developers to clearly separate the refactored CSS codebase from the legacy codebase, but also to quickly show the progress to the project managers and other project stakeholders on a per-page basis.

For example, management may decide to test the effects of the refactored codebase early by deploying only the refactored homepage code and they would want to know when the homepage components will be refactored and ready for A/B testing.

Instead of wasting some time comparing the homepage components with the available tasks on the Kanban board, developers can just temporarily add the following styles to highlight the refactored components which have the rf- prefix in their class names, and the components that need to be refactored. That way, they can reorganize the tasks and prioritize refactoring homepage components.

/* Highlights all refactored components */
[class*=”rf-“] {
outline: 5px solid green;
}

/* Highlights all components that havent been refactored */
[class]:not([class*=”rf-“]),
body *:not([class]) {
outline: 5px solid red;
}

Maintaining The Refactored Codebase

After the refactoring project has been completed, the team needs to make sure to maintain the codebase health for the foreseeable future — new features will be developed, some new features might even be rushed and produce technical debt, various bugfixes will also be developed, etc. All in all, the development team needs to make sure that the codebase health remains stable as long as they’re in charge of the codebase.

Technical debt which can result in potentially faulty CSS code should be isolated, documented, and implemented in a separate CSS file which is often named as shame.css.

It’s important to document the rules and best practices that were established and applied during the refactoring projects. Having those rules in writing allows for standardized code reviews, faster project onboarding for new team members, easier project handoff, etc.

Some of the rules and best practices can also be enforced and documented with automated code-checking tools like stylelint. Andrey Sitnik, the author of widely-used CSS development tools like PostCSS and Autoprefixer, has noted how automatic linting tools can make code reviews and onboarding easier and less stressful.

“However, automatic linting is not the only reason to adopt Stylelint in your project. It can be extremely helpful for onboarding new developers on the team: a lot of time (and nerves!) are wasted on code reviews until junior developers are fully aware of accepted code standards and best practices. Stylelint can make this process much less stressful for everyone.”

Additionally, the team can create a Pull Request template and include the checklist of standards and best practices and a link to the project code rules document so that the developers making the Pull Request can check the code themselves and make sure that it follows the agreed standards and best practices.

Conclusion

Incremental refactoring strategy is one of the safest and most recommended approaches when it comes to refactoring CSS. The development team needs to refactor the codebase component by component to ensure that the tasks have a low scope and are manageable. Individual components need to be then developed in isolation — away from the faulty code — and then merged with the existing codebase. The issues that may come up from the conflicting codebases can be solved by adding a temporary CSS file that contains all the necessary overrides to remove the conflicts in CSS styles. After that, legacy code for the target component can be removed and the process continues until all components have been refactored and until the temporary CSS file which contains the overrides is empty.

Visual regression testing tools like Percy and Chromatic can be used for testing and to detect any regressions and unwanted changes on the Pull Request level, so developers can fix these issues before the refactored code is deployed to the live site.

Developers can use A/B testing and use monitoring tools to make sure that the refactoring doesn’t negatively affect performance and user experience before finally launching the refactored project on a live site. Developers will also need to ensure that the agreed standards and best practices are used on the project continues to maintain the codebase health and quality in the future.

References

Refactoring CSS Without Losing Your Mind,” Harry Roberts (Video, WeAreDevelopers Conference, 2017)
Large Scale CSS Refactoring At Trivago,” Christoph Reinartz
Refactoring Tunnels,” Harry Roberts
Sundance.org Design System And CSS Refactor,” Kirby Yardley
Five Years Of PostCSS: State Of The Union,” Andrey Sitnik