Frontend Refactoring

0 min read

Leah Wagner

How many times do we review code we wrote 2 years ago or even 2 months ago and begin to question how we approached a certain solution? There are countless reasons for this and none of them involve you being a terrible developer.

You may have been writing code based on certain constraints that had been outlined as a part of this project: user demographics, technology stack, development philosophy, budgets and deadlines among many other things. All those factors influence the decisions we make as we write code and (insert dramatic music here) these constraints are rarely constant over time.

All these evolving constraints — and one's awareness of their evolution — triggers the need to refactor elements of the codebase on projects requiring ongoing maintenance. Refactoring helps our projects stay up to date with current code standards and technologies but more importantly to keep up with the current project's constraints.

Code archeology: a necessary first step

Before any code changes, we need to review and understand it in it’s current state. Perhaps you have well decoupled components to work with, hopefully you have amazing documentation, but in some unfortunate cases, you may have none of those. Whichever it is, at this point, it is key to spend the time needed to understand the context in which it was written and goals it achieved. In my experience, without this step, one can never have the confidence they need to liberally hack, slash and refactor in a way the project may require.

Enough of the theoretical: Here is what we did!

The catalyst

We found a bug in the mobile navigation. The site had a fixed navigation that would stay at the top of the page as the user scrolled. This carried over to the mobile layout where we also converted the menu into a dropdown. Everything was working great until we flipped the phone into a landscape orientation. Fixed nav + dropdown menu + 320px height = inaccessible menu items. As we started to troubleshoot, our initial inclination was to add code to fix the issue. Maybe have a special media query and styles for landscape phones or add overflow:scroll when needed -- none of which felt ideal.

It was becoming apparent that the Javascript powering the mobile menu would have to get more complicated to fix this issue. Then we asked ourselves: why are we doing all of this with JS when we have CSS animations at our disposal? Thus, our archeological dig transitioned to the CSS, and looking closer, we began to see how a new implementation could greatly improve how maintainable this code would be moving forward -- maybe we could even get some performance gains with less JavaScript.

The constraints

Here are some of the constraints we were originally working to and how they have evolved today:

1. Changing technologies

Then: We only had 2x retina images… those were the days.Now: Today we also have 3x and 4x so our 2x images weren’t looking their best on those devices.

2. Website traffic

Then: IE8 traffic was significant and deemed important. This greatly affected how we wrote our CSS.Now: Today, this has traffic has quickly evolved to be predominantly iOS. With less importance placed on IE8, we can now approach our CSS and animations in a very different way.

3. Importance of esthetics

Then: Being a luxury brand, how the site looked was among the highest of importance.Now: Esthetics are still incredibly important, but today, we are better at factoring in the performance implications for these decisions.

4. Ongoing development

Then: We like to remain agile while developing a new site. This allows us to remain responsive to changes and modifications from our clients. However, this also makes it harder to decouple code that is constantly changing.Now: Today, working with a relatively stable build, we now have an improved understanding of the code base and an opportunity to refine our work.

The goal

Plain and simple: stable, predictable and easy to update code. At times, we were getting that fragile feeling whenever we had to dive in and make edits — there was definitely room for simplification that would give us not only greater confidence but also increase our productivity and more reliable outcomes.

The improvements

Based in the factors above, we implemented the following improvements.

1. Retina images > SVG

In the navigation, we had a retina image being loaded through a media query that detected 2x screens. This worked great originally, but now we have 3x and 4x screens added to the mix.

Instead of creating and maintaining 3x and 4x images, we decided to use SVG for graphic based images. This way, we would only ever need two images -- a single SVG and a PNG fallback for IE8 and older Android browsers. With this approach, there is no media query for SVG support. Instead, we leaned on Moderizr.js which applies an .svg class to our html element when it is supported.

So our code now looked something like this:

li a.main-menu--link-logo { width: 213px; height: 112px; background: transparent url('../images/menu/logo.png') no-repeat center center; html.svg & { background-image: url('../images/menu/logo.svg'); } }

2. Sitewide breakpoints > Component specific breakpoints

Originally we developed the navigation to fit with breakpoints we had defined for the page layout: 0px, 480px and 768px. With this, we found ourselves adjusting font sizes in our menu to fit within these breakpoints. Hindsight says this is not how we should be make font size decisions.

Instead, we serve the ideal font size for legibility and, when the layout no longer accommodates this font size, we simply change it. This means that we defined custom breakpoints specifically for the navigation component. This approach saved us a handful of font size modifications that were happening across breakpoints. The screenshots below show how our new navigation breakpoints are in no way related to the site layout breakpoints defined above.

Breakpoint #1: Base, 0px Breakpoint #2: 748px

Breakpoint #3: 948px

3. Using CSS inheritance with smarter overrides

The very nature of CSS is that it Cascades, or inherits, properties when they are applied parent elements. CSS also allows us to combine certain properties into a single line. This is most commonly seen with the border and background properties. For example, this shows two ways a border can be defined:

// single line .border-example { border: solid 1px #000; } // separate lines .border-example { border-style: solid; border-width: 1px; border-color: #000; }

Defining all of your properties in a single line definitely improves the readability of your CSS. However, when applying overrides that depend on inheritance, we are now aiming to only override the property that needs adjustment. You will see this technique in the SVG example we show above. When we change the image for browsers that support SVG, we are only overriding the background-image property. We aren’t redefining all elements that were defined in the initial background declaration. So in this case, we allow the SVG background-image to inherit “no-repeat center center” from the original declaration. In the future, if the background position needs to be updated to “center top”, we only need to change this in one spot.

Using this technique led us to another improvement for the navigation code. The navigation has two different colour options based on the background image being used.

primary display

override for light backgrounds

Below is a simplified example of how the original code was formatted.

// primary display #main-menu ul.menu li a { color: #FFF border-bottom: solid 1px #FFF; padding-bottom: 12px; &:hover, &.active { border-bottom: solid 2px #FFF; padding-bottom: 11px; } } // override for light backgrounds html.dark-nav #main-menu ul.menu li a { color: #445a7c; border-bottom: solid 1px #445a7c; padding-bottom: 12px; &:hover, &.active { border-bottom: solid 2px #445a7c; padding-bottom: 11px; } }

Adjusting two simple lines removes the need for several others because the desired attributes will be properly inherited from the primary display. With this new code, colours and border widths can be updated in one spot and then they will be inherited by all displays.

// primary display #main-menu ul.menu li a { color: #FFF border-bottom: solid 1px #FFF; padding-bottom: 12px; &:hover, &a.active { border-bottom-width: 2px; padding-bottom: 11px; } } // override for light backgrounds html.dark-nav #main-menu ul.menu li a { color: #445a7c; border-color: #445a7c; }

4. JS-based animation > CSS-based animation

For our mobile menu expand/hide animation, we removed JS SlideToggle and updated the JS to simply toggle an ‘is-visible’ class on the menu wrapper. We can then use CSS to animate the height based on whether that class is there or not. This worked well in this case because our navigation has a predictable height. (A well documented issue for animating CSS height is that you can’t go from 0px to auto).

This change came at a cost: while our JS based toggle animation worked in IE9 and IE8, our new CSS based approach does not. With our client's visitor stats categorically trending down on those two browsers, we chose the performance win and code clarity the CSS approach gave us.

#mobile-menu { width: 100%; height: 0px; @include transition(height 0.3s); &.is-visible { height: 75px } }

The results

As a result of our changes, we were able to make our JavaScript more concise and rely on CSS to do more. The code required for the menu component cut over 100 lines of JS and 300 lines of CSS — even after moving the animation logic to CSS. Also with custom breakpoints and componentized SASS partials, our navigation code now lives in a neat decoupled package of it’s own. When the next person comes along to manipulate this code, their code discovery phase will only involve looking as this small part of the code base. Ultimately these benefits appear to be developer centric. However, these do pass down to the client as well. A code base that is easier to understand gets developers into writing the new code that matters in a more timely manner.

The business case

A refactoring project does not have to involve rewriting your entire application or website. That can be pretty hard to sell -- either internally or to a client. In this case, we were working on an implementation of a homepage redesign. Within that timeframe we were able to group in this navigation refactoring. This would not have been possible if we had determined the refactoring project was much larger.

With the end goal being to leave a project in a better place than you started, improve what you can and decouple your optimized components from older legacy code. Piece by piece the site will become easier to maintain.

This blog is based on a presentation given by Leah Wagner at the 2015 Pacific Northwest Drupal Summit in Seattle. View presentation slides.

Be notified of our future tech blogs by following us on Twitter.