Modernizing Web Application JavaScript

Refactoring is a process of revisiting code and attempting to make it ‘better’. The code is no longer useful due to its age, functionality, or preference and it has been decided to be replaced with an equal set of ‘better’ code. At no point does a developer learn the best way to refactor code. Refactoring is a skill learned through experience. JavaScript refactoring is notoriously hard due to scope and dynamic accessing of object properties. There is a clear pattern, however, that can be followed that promotes success and is pragmatic.

Apply syntax rules

Whether using jslint, jshint, eslint or jscs; automating the removal of simple bugs is a critical step. It can and probably should be broken up into smaller chunks by applying rules a few at time. A few rules that will help and drive future refactoring and are highly recommended are strict mode, requiring semicolons, not mixing tab and spaces, deleting unused variables, requiring block statements and type equality. Once syntax changes are implemented, enforce them with hooks to the code repository and/or part of the build process. Git, Subversion and Mercurial all implement hooks that are perfect for this. Your builds in grunt can also deal with git hooks too.

Apply closures ( skip if already in a module format )

Closures are not the final form but as a professional, pragmatic developer, the goal is to make incremental steps that can be released. By not allowing global scope inside the closure, you can emulate imports (the arguments) and exports (global namespace additions). The code should split up into 1 file per globally name-spaced “exported” object or function. Surprisingly, the code still hasn’t logically changed ( unless you were abusing a syntax error ). There could be a lot of files now if splitting them up when closuring (recommended), where before there only had a few.

The code is following a script loading procedure. JavaScript will load and run files in the order they are dropped on the page. Related, each request for a file slows down the initial page load due to the extra round trip HTTP calls. It should be obvious then to concatenate files. But now that the files are in closures, with strict mode, the values passed into the closure might be undefined depending on where they are loaded ( you can’t import a value that hasn’t been set yet, so it is undefined ). The order the code is concatenated in ( or loaded on the page in ) matters greatly. When using a build tool, make sure to name the files in an alphanumerical order that ensures values are set before included.
This necessary step has the potential clean up code dramatically without requiring logical changes to the code.

Start adding unit tests

It isn’t safe to rewrite code if without knowing what the code’s input and output is. Unit testing is ubiquitous in JavaScript and there are more than a few frameworks for this. Adding and maintaining tests is an ongoing step, but its important that it’s possible to add a test easily to the code and run it before beginning logic changes. Begin with setting up the ability to test and then write a few tests. Add headless testing to a Push Hook or during a build process after the linting code. Make it a habit to write a test for complicated code before reworking. Karma and Testswarm make this easy to test in browsers as well (and can be extended to SauceLabs or BrowserStack). Unit testing should be at the functional level. If you feel you also want to take it to the next level with integration testing, then you can take a look at my favorite, Nightwatch.

Initialize code purposefully

Proper code waits to be initialized and not run immediately on page load. Immediate invocation of code does introduce race conditions and has a lot of sides effects. The first issue most developers run into is running JavaScript before the DOM is ready and then wondering why there are errors when looking for elements. In jQuery, you can add code like that in a $.ready block that waits for the DOM load event to trigger. But that is still not enough, because some code will run before the rest of the code has been loaded. It’s best that all app code can be loaded without actually running any code. Then, trigger the code to begin initializing. That means, if the code was on a blank page, nothing would actually run. No code would initialize, until it was told to. This also allows for much better unit testing now that the code doesn’t run unless it’s been initialized so the test environment cleans up right away.

Begin deleting unused code

In the introduction I stated that JavaScript is very difficult to refactor due to its dynamic parameter nature. That makes this step dangerous. If you have been following along, your JavaScript is now either in modules or single file closures. We now know each files input and output (what’s outside code is being used, and what the code is exposing to others). Deleting code is a manual process of removing files that are never imported by others at the highest level. But wait, there is a big gotcha. JavaScript allows for functions and values to be dynamically accessed; so even if myFunc doesn’t exist, my+Func might! A simple search/replace on the code is not enough. So be careful when removing functions that might not be used. It is easy to delete code that is used without finding a place it’s being used. Doing a ‘debugger;’ statement and then running the application’s functionality would help reassure that code is not being triggered (assuming you can trigger all of the functionality easily). Or add logging notes and watch for usage in the logs. Also if code is running prior to initialization, then regardless if no other code is importing it, then it cannot be safely removed until debugged.

Perform statistical analysis

Many red flags in JavaScript are hidden and static analysis (SA) can be used to find the code that needs refactored first. Code complexity can be tracked, as well as a maintainability and parameter usage. SA should be in the build or continuous integration process. It helps guide where code is needing the most attention without guessing or randomly refactoring. With JavaScript being such a magnet for young developers, the science of SA has not been at the forefront. That condition has quickly changed, and now it’s very hard to argue that SA isn’t useful or needed. Simple, useful tools are freely available like Plato and SonarQube can be used to track high level stats and to even set limits to certain things (like how many lines a function can be). SA is also useful for things like code coverage (or even the linting we did earlier) in tools like Istanbul.

Add logging

Before doing any large scale, breaking changes, pay attention to the production ready code. Logging for errors and having a visual layer to perceive errors over time is something any size team will benefit from. When changing out large swaths of code, there will always be a breakage somewhere you aren’t expecting and tracking the errors will highlight that. NewRelic provides common tools to get the job done, but this is also one of those areas where rolling your own isn’t too difficult.

Create many small functions from large functions

Using the statistical analysis, you should be able to find high cyclomatic complex functions. Begin turning these into multiple functions that call each other, rather than a single large function. Try to find the generic patterns in the code so generic, testable functions can be created. Make sure to write tests too. Don’t worry just yet about completely refactoring the logic because it’s still not going to be entirely clear what the code is attempting to do in all cases. Focus on making small testable functions. Take notes for odd code or code that you feel needs revisited. It will take a long time to get to small, purposeful functions, but keep at it.

Modularize the code

The code is probably modular in a sense at this point, but it’s important to apply 2015+ best practices. Look at the ES6 module structure and look at using transpilers like Babel or SystemJS to rewrite your code in a module format. Still valid, but less future proof, would be using CommonJS (Node style modules) or Require with a Browserify step. In any case, going from script loading to module loading is a key performance and structural change to the code. It’s not a step that can be done quickly and without error; which is why all the previous work was done. However, once using best practices with modules, the code becomes much more manageable and adaptable for any future efforts.

Rethink

The code is modular, small, syntax free and known. The last set of tasks is to basically re-think the application. The nitty-gritty implementation details should be rethought for clarity and performance. Are the events properly bound in the DOM? Is a pubsub model in place for non-DOM eventing? Is the data immutable? The amount of work could be endless, but it’s important to understand that these questions are only now possible due to the heavy work done already. The argument is no longer revolving around making better code, it’s now making smarter code. It’s a continuous process, but we’ve hit all the high level architecture changes that provide a framework for the really deep questions on performance and application hierarchy. Go forth and improve.