Please note that all the code examples for this chapter are available as a part of this chapter's code download on the book's website at www.wrox.com/go/projavascript4e
on the Download Code tab.
The discipline of web development has grown at an extraordinary rate since 2000. What used to be a virtual Wild West, where just about anything was acceptable, has evolved into a complete discipline with research and established best practices. As simple websites grew into more complex web applications, and web hobbyists became paid professionals, the world of web development was filled with information about the latest techniques and development approaches. JavaScript, in particular, was the beneficiary of a lot of research and conjecture. Best practices for JavaScript fall into several categories and are handled at different points in the development process.
In early websites, JavaScript was used primarily for small effects or form validation. Today's web applications are filled with thousands of lines of JavaScript executing all types of complicated processes. This evolution requires that developers take maintainability into account. As with software engineers in more traditional disciplines, JavaScript developers are hired to create value for their company, and they do that not just by delivering products on time but also by developing intellectual property that continues to add value long after.
Writing maintainable code is important because most developers spend a large amount of their time maintaining other people's code. It's a truly rare occurrence to be able to develop new code from scratch; it's often the case that you must build on work that someone else has done. Making sure that your code is maintainable ensures that other developers can perform their jobs as well as possible.
Maintainable code has several characteristics. In general, code is said to be maintainable when it is all of the following:
Being able to write maintainable JavaScript code is an important skill for professionals. This is the difference between hobbyists who hack together a site over the weekend and professional developers who really know their craft.
One of the simplest ways to start writing maintainable code is to come up with code conventions for the JavaScript that you write. Code conventions have been developed for most programming languages, and a quick Internet search is likely to turn up thousands of documents. Professional organizations have long instituted code conventions for developers in an attempt to make code more maintainable for everyone. The best-run open-source projects have strict code convention requirements that allow everyone in the community to easily understand how code is organized.
Code conventions are important for JavaScript because of the language's adaptability. Unlike most object-oriented languages, JavaScript doesn't force developers into defining everything as objects. The language can support any number of programming styles, from traditional object-oriented approaches to declarative approaches to functional approaches. A quick review of several open-source JavaScript libraries can easily yield multiple approaches to creating objects, defining methods, and managing the environment.
The following sections discuss the basics of how to develop code conventions. These topics are important to address, although the way in which they are addressed may differ, depending on your individual needs.
For code to be maintainable, it must first be readable. Readability has to do with the way the code is formatted as a text file. A large part of readability has to do with the indentation of the code. When everyone is using the same indentation scheme, code across an entire project becomes much easier to read. Indentation is usually done by using a number of spaces instead of by using the tab character, which is typically displayed differently by different text editors. A good general indentation size is four spaces, although you may decide to use less or more.
Another part of readability is comments. In most programming languages, it's an accepted practice to comment each method. Because of JavaScript's ability to create functions at any point in the code, this is often overlooked. Because of this, it is perhaps even more important to document each function in JavaScript. Generally speaking, the places that should be commented in your code are as follows:
Indentation and comments create more readable code that is easier to maintain in the future.
The proper naming of variables and functions in code is vital to making it understandable and maintainable. Because many JavaScript developers began as hobbyists, there's a tendency to use nonsensical names such as "foo"
and "bar"
for variables and names such as "doSomething"
for functions. A professional JavaScript developer must overcome these old habits to create maintainable code. General rules for naming are as follows:
"car"
or "person"
.getName()
. Functions that return Boolean values typically begin with is
, as in isEnabled()
.getName()
and isPerson
. Classes like Person
and RequestFactory
should be capitalized. Constant values should be all uppercase and underscores, such as REQUEST_TIMEOUT
.getName()
intuitively will return a name value. PersonFactory
will be producing some sort of Person
object or entity.It's imperative to avoid useless variable names that don't indicate the type of data they contain. With proper naming, code reads like a narrative of what is happening, making it easier to understand.
Because variables are loosely typed in JavaScript, it is easy to lose track of the type of data that a variable should contain. Proper naming mitigates this to some point, but it may not be enough in all cases. There are three ways to indicate the data type of a variable.
The first way is through initialization. When a variable is defined, it should be initialized to a value that indicates how it will be used in the future. For example, a variable that will hold a Boolean should be initialized to either true
or false
, and a variable to hold numbers should be initialized to a number, as in the following example:
// variable type indicated by initialization
let found = false; // Boolean
let count = -1; // number
let name = ""; // string
let person = null; // object
Initialization to a particular data type is a good indication of a variable's type. The downside of initialization is that it cannot be used with function arguments in the function declaration.
The second way to indicate a variable's type is to use Hungarian notation. Hungarian notation prepends one or more characters to the beginning of a variable to indicate the data type. This notation is popular among scripted languages and was, for quite some time, the preferred format for JavaScript as well. The most traditional Hungarian notation format for JavaScript prepends a single character for the basic data types: "o"
for objects, "s"
for strings, "i"
for integers, "f"
for floats, and "b"
for Booleans. Here's an example:
// Hungarian notation used to indicate data type
let bFound; // Boolean
let iCount; // integer
let sName; // string
let oPerson; // object
Hungarian notation for JavaScript is advantageous in that it can be used equally well for function arguments. The downside of Hungarian notation is that it makes code somewhat less readable, interrupting the intuitive, sentence-like nature of code that is accomplished without it. For this reason, Hungarian notation has started to fall out of favor among some developers.
The last way to indicate variable type is to use type comments. Type comments are placed right after the variable name but before any initialization. The idea is to place a comment indicating the data type right by the variable, as in this example:
// type comments used to indicate type
let found /*:Boolean*/ = false;
let count /*:int*/ = 10;
let name /*:String*/ = "Nicholas";
let person /*:Object*/ = null;
Type comments maintain the overall readability of code while injecting type information at the same time. The downside of type comments is that you cannot comment out large blocks of code using multiline comments because the type comments are also multiline comments that will interfere, as this example demonstrates:
// The following won't work correctly
/*
let found /*:Boolean*/ = false;
let count /*:int*/ = 10;
let name /*:String*/ = "Nicholas";
let person /*:Object*/ = null;
*/
Here, the intent was to comment out all of the variables using a multiline comment. The type comments interfere with this because the first instance of /*
(second line) is matched with the first instance of */
(third line), which will cause a syntax error. If you want to comment out lines of code using type comments, it's best to use single-line comments on each line (many editors will do this for you).
These are the three most common ways to indicate the data type of variables. Each has advantages and disadvantages for you to evaluate before deciding on one. The important thing is to decide which works best for your project and use it consistently.
Whenever parts of an application depend too closely on one another, the code becomes too tightly coupled and hard to maintain. The typical problem arises when objects refer directly to one another in such a way that a change to one always requires a change to the other. Tightly coupled software is difficult to maintain and invariably has to be rewritten frequently.
Because of the technologies involved, there are several ways in which web applications can become too tightly coupled. It's important to be aware of this and to try to maintain loosely coupled code whenever possible.
One of the most common types of coupling is HTML/JavaScript coupling. On the web, HTML and JavaScript each represent a different layer of the solution: HTML is the data, and JavaScript is the behavior. Because they are intended to interact, there are a number of different ways to tie these two technologies together. Unfortunately, there are some ways that too tightly couple HTML and JavaScript.
JavaScript that appears inline in HTML, either using a <script>
element with inline code or using HTML attributes to assign event handlers, is too tightly coupled. Consider the following code examples:
<!-- tightly coupled HTML/JavaScript using <script> -->
<script>
document.write("Hello world!");
</script>
<!-- tightly coupled HTML/JavaScript using event handler attribute -->
<input type="button" value="Click Me" onclick="doSomething()"/>
Although these are both technically correct, in practice they tightly couple the HTML representing the data with the JavaScript that defines the behavior. Ideally, HTML and JavaScript should be completely separate, with the JavaScript being included via external files and attaching behavior using the DOM.
When HTML and JavaScript are too tightly coupled, interpreting a JavaScript error means first determining whether the error occurred in the HTML portion of the solution or in a JavaScript file. It also introduces new types of errors related to the availability of code. In this example, the button may be clicked before the doSomething()
function is available, causing a JavaScript error. Maintainability is affected because any change to the button's behavior requires touching both the HTML and the JavaScript, when it should require only the latter.
HTML and JavaScript can also be too tightly coupled when the reverse is true: HTML is contained within JavaScript. This usually occurs when using innerHTML
to insert a chunk of HTML text into the page, as in this example:
// tight coupling of HTML to JavaScript
function insertMessage(msg) {
let container = document.getElementById("container");
container.innerHTML = `<div class="msg">
<p> class="post">${msg}</p>
<p><em>Latest message above.</em></p>
</div>`;
}
Generally speaking, you should avoid creating large amounts of HTML in JavaScript. This, once again, has to do with keeping the layers separate and being able to easily identify the source of errors. When using this example code, a problem with page layout may be related to dynamically created HTML that is improperly formatted. However, locating the error may be difficult because you would typically first view the source of the page to look for the offending HTML but wouldn't find it there because it's dynamically generated. Changes to the data or layout would also require changes to the JavaScript, which indicates that the two layers are too tightly coupled.
HTML rendering should be kept separate from JavaScript as much as possible. When JavaScript is used to insert data, it should do so without inserting markup whenever possible. Markup can typically be included and hidden when the entire page is rendered such that JavaScript can be used to display the markup later, instead of generating it. Another approach is to make an Ajax request to retrieve additional HTML to be displayed; this approach allows the same rendering layer (PHP, JSP, Ruby, and so on) to output the markup, instead of embedding it in JavaScript.
Decoupling HTML and JavaScript can save time during debugging by making it easier to identify the source of errors, and it also eases maintainability: changes to behavior occur only in JavaScript files, whereas changes to markup occur only in rendering files.
Another layer of the web tier is CSS, which is primarily responsible for the display of a page. JavaScript and CSS are closely related: they are both layers on top of HTML and as such are often used together. As with HTML and JavaScript, however, it's possible for CSS and JavaScript to be too tightly coupled. The most common example of tight coupling is using JavaScript to change individual styles, as shown here:
// tight coupling of CSS to JavaScript
element.style.color = "red";
element.style.backgroundColor = "blue";
Because CSS is responsible for the display of a page, any trouble with the display should be addressable by looking just at CSS files. However, when JavaScript is used to change individual styles, such as color, it adds a second location that must be checked and possibly changed. The result is that JavaScript is somewhat responsible for the display of the page and a tight coupling with CSS. If the styles need to change in the future, both the CSS and the JavaScript files may require changes. This creates a maintenance nightmare for developers. A cleaner separation between the layers is needed.
Modern web applications use JavaScript to change styles frequently, so although it's not possible to completely decouple CSS and JavaScript, the coupling can be made looser. This is done by dynamically changing classes instead of individual styles, as in the following example:
// loose coupling of CSS to JavaScript
element.className = "edit";
By changing only the CSS class of an element, you allow most of the style information to remain strictly in the CSS. JavaScript can be used to change the class, but it's not directly affecting the style of the element. As long as the correct class is applied, then any display issues can be tracked directly to CSS and not to JavaScript.
Once again, the importance of keeping a good separation of layers is paramount. The only source for display issues should be CSS, and the only source for behavior issues should be JavaScript. Keeping a loose coupling between these layers makes your entire application more maintainable.
Every web application is typically filled with lots of event handlers listening for numerous different events. Few of them, however, take care to separate application logic from event handlers. Consider the following example:
function handleKeyPress(event) {
if (event.keyCode == 13) {
let target = event.target;
let value = 5 * parseInt(target.value);
if (value> 10) {
document.getElementById("error-msg").style.display = "block";
}
}
}
This event handler contains application logic in addition to handling the event. The problem with this approach is twofold. First, there is no way to cause the application logic to occur other than through the event, which makes it difficult to debug. What if the anticipated result didn't occur? Does that mean that the event handler wasn't called or that the application logic failed? Second, if a subsequent event causes the same application logic to occur, you'll need to duplicate the functionality or else extract it into a separate function. Either way, it requires more changes to be made than are really necessary.
A better approach is to separate the application logic from event handlers, so that each handles just what it's supposed to. An event handler should interrogate the event
object for relevant information and then pass that information to some method that handles the application logic. For example, the previous code can be rewritten like this:
function validateValue(value) {
value = 5 * parseInt(value);
if (value> 10) {
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event) {
if (event.keyCode == 13) {
let target = event.target;
validateValue(target.value);
}
}
This updated code properly separates the application logic from the event handler. The handleKeyPress()
function checks to be sure that the Enter key was pressed (event.keyCode
is 13
) and then gets the target of the event and passes the value property into the validateValue()
function, which contains the application logic. Note that there is nothing in validateValue()
that depends on any event handler logic whatsoever; it just receives a value and can do everything else based on that value.
Separating application logic from event handlers has several benefits. First, it allows you to easily change the events that trigger certain processes with a minimal amount of effort. If a mouse click initially caused the processing to occur, but now a key press should do the same, it's quite easy to make that change. Second, you can test code without attaching events, making it easier to create unit tests or to automate application flow.
Here are a few rules to keep in mind for loose coupling of application and business logic:
event
object into other methods; pass only the data from the event
object that you need.Keeping this approach in mind is a huge maintainability win in any code base, opening up numerous possibilities for testing and further development.
Writing maintainable JavaScript isn't just about how the code is formatted; it's also about what the code does. Web applications created in an enterprise environment are often worked on by numerous people at the same time. The goal in these situations is to ensure that the browser environment in which everyone is working has constant and unchanging rules. To achieve this, developers should adhere to certain programming practices.
The dynamic nature of JavaScript means that almost anything can be modified at any point in time. It's been said that nothing in JavaScript is sacred, as you're unable to mark something as final or constant. This changed somewhat with ECMAScript 5's introduction of tamper-proof objects, but by default, all objects can be modified. In other languages, objects and classes are immutable when you don't have the actual source code. JavaScript allows you to modify any object at any time, making it possible to override default behaviors in unanticipated ways. Because the language doesn't impose limits, it's important and necessary for developers to do so.
Perhaps the most important programming practice in an enterprise environment is to respect object ownership, which means that you don't modify objects that don't belong to you. Put simply: if you're not responsible for the creation or maintenance of an object, its constructor, or its methods, you shouldn't be making changes to it. More specifically:
The problem is that developers assume that the browser environment works in a certain way. Changes to objects that are used by multiple people mean that errors will occur. If someone expects a function called stopEvent()
to cancel the default behavior for an event, and you change it so it does that and also attaches other event handlers, it is certain that problems will follow. Other developers are assuming that the function just does what it did originally, so their usage will be incorrect and possibly harmful because they don't know the side effects.
These rules apply not only to custom types and objects but also to native types and objects such as Object
, String
, document
, window
, and so on. The potential issues here are even more perilous because browser vendors may change these objects in unannounced and unanticipated ways.
An example of this occurred in the popular Prototype JavaScript library, which implemented the getElementsByClassName()
method on the document
object, returning an instance of Array
that had also been augmented to include a method called each()
. John Resig outlined on his blog the sequence of events that caused the issue. In his post (http://ejohn.org/blog/getelementsbyclassname-pre-prototype-16/
), he noted that the problem occurred when browsers began to natively implement getElementsByClassName()
, which returns not an Array
but rather a NodeList
that doesn't have an each()
method. Developers using the Prototype library had gotten used to writing code such as this:
document.getElementsByClassName("selected").each(Element.hide);
Although this code worked fine in browsers that didn't implement getElementsByClassName()
natively, it caused an error in the ones that did, as a result of the return value differences. You cannot anticipate how browser vendors will change native objects in the future, so modifying them in any way can lead to issues down the road when your implementation clashes with theirs.
The best approach, therefore, is to never modify objects you don't own. You own an object only when you created it yourself, such as a custom type or object literal. You don't own Array
, document
, and so on, because they were there before your code executed. You can still create new functionality for objects by doing the following:
Many JavaScript libraries now subscribe to this theory of development, allowing them to grow and adapt even as browsers continually change.
Closely related to respecting object ownership is avoiding global variables and functions whenever possible. Once again, this has to do with creating a consistent and maintainable environment in which scripts will be executed. At most, a single global variable should be created on which other objects and functions exist. Consider the following:
// two globals - AVOID!!!
var name = "Nicholas";
function sayName() {
console.log(name);
}
This code contains two globals: the variable name
and the function sayName()
. These can easily be created on an object that contains both, as in this example:
// one global - preferred
var MyApplication = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};
This rewritten version of the code introduces a single global object, MyApplication
, onto which both name
and sayName()
are attached. Doing so clears up a couple of issues that existed in the previous code. First, the variable name
overwrites the window.name
property, which possibly interferes with other functionality. Second, it helps to clear up confusion over where the functionality lives. Calling MyApplication.sayName()
is a logical hint that any issues with the code can be identified by looking at the code in which MyApplication
is defined.
An extension of the single global approach is the concept of namespacing. Namespacing involves creating an object to hold functionality. The Google Closure library utilizes namespaces to organize its contents. Here are some examples:
goog.string
—Methods for manipulating strings.goog.html.utils
—Methods for working with HTML.goog.i18n
—Methods for helping with internationalization (i18n).The single global object goog
serves as a container onto which other objects are defined. Whenever objects are used simply to group together functionality in this manner, they are called namespaces. The entire Google Closure library is built on this concept, allowing it to coexist on the same page with any other JavaScript library.
The important part of namespacing is to decide on a global object name that everyone agrees to use and that is unique enough that others aren't likely to use it as well. In most cases, this can be the name of the company for which you're developing the code, such as goog
or Wrox
. You can then start creating namespaces to group your functionality, as in this example:
// create global object
var Wrox = {};
// create namespace for Professional JavaScript
Wrox.ProJS = {};
// attach other objects used in the book
Wrox.ProJS.EventUtil = { … };
Wrox.ProJS.CookieUtil = { … };
In this example, Wrox
is the global on which namespaces are created. If all code for this book is placed under the Wrox.ProJS
namespace, it leaves other authors to add their code onto the Wrox
object as well. As long as everyone follows this pattern, there's no reason to be worried that someone else will also write an object called EventUtil
or CookieUtil
because it will exist on a different namespace. Consider this example:
// create namespace for Professional Ajax
Wrox.ProAjax = {};
// attach other objects
Wrox.ProAjax.EventUtil = { … };
Wrox.ProAjax.CookieUtil = { … };
// you can still access the ProJS one
Wrox.ProJS.EventUtil.addHandler( … );
// and the ProAjax one separately
Wrox.ProAjax.EventUtil.addHandler( … );
Although namespacing requires a little more code, it is worth the trade-off for maintainability purposes. Namespacing helps ensure that your code can work on a page with other code in a nonharmful way.
Because JavaScript doesn't do any automatic type checking, it becomes the developer's responsibility. As a result, very little type checking actually gets done in JavaScript code. The most common type check is to see if a value is null
. Unfortunately, checking a value against null
is overused and frequently leads to errors due to insufficient type checking. Consider the following:
function sortArray(values) {
if (values != null) { // AVOID!!
values.sort(comparator);
}
}
The purpose of this function is to sort an array with a given comparator. The values
argument must be an array for the function to execute correctly, but the if
statement simply checks to see that values
isn't null
. There are several values that can make it past the if
statement, including any string or any number, which would then cause the function to throw an error.
Realistically, null
comparisons are rarely good enough to be used. Values should be checked for what they are expected to be, not for what they aren't expected to be. For example, in the previous code, the values
argument is expected to be an array, so you should be checking to see if it is an array, rather than checking to see if it's not null
. The function can be rewritten more appropriately as follows:
function sortArray(values) {
if (values instanceof Array) { // preferred
values.sort(comparator);
}
}
This version of the function protects against all invalid values and doesn't need to use null
at all.
If you see a null
comparison in code, try replacing it using one of the following techniques:
instanceof
operator to check its constructor.typeof
operator to check its type.typeof
operator to ensure that a method with the given name exists on the object.The fewer null
comparisons in code, the easier it is to determine the purpose of the code and to eliminate unnecessary errors.
The goal of relying on constants is to isolate data from application logic in such a way that it can be changed without risking the introduction of errors. Strings that are displayed in the user interface should always be extracted in such a way as to allow for internationalization. URLs should also be extracted because they have a tendency to change as an application grows. Basically, each of these has a possibility of changing for one reason or another, and a change would mean going into the function and changing code there. Any time you're changing application logic code, you open up the possibility of creating errors. You can insulate application logic from data changes by extracting data into constants that are defined separately.
The key is to separate data from the logic that uses it. The types of values to look for are as follows:
Using constants is an important technique for enterprise JavaScript development because it makes code more maintainable and keeps it safe from data changes.
The amount of JavaScript that developers now write per web page has grown dramatically since the language was first introduced. With that increase came concerns over the runtime execution of JavaScript code. JavaScript was originally an interpreted language, so the speed of execution was significantly slower than it was for compiled languages. Chrome was the first browser to introduce an optimizing engine that compiles JavaScript into native code. Since then, all other major browsers have followed suit and have implemented JavaScript compilation.
Even with the move to compiled JavaScript, it's still possible to write slow code. However, there are some basic patterns that, when followed, ensure the fastest possible execution of code.
The “Variables, Scope, and Memory” chapter discussed the concept of scopes in JavaScript and how the scope chain works. As the number of scopes in the scope chain increases, so does the amount of time it takes to access variables outside of the current scope. It is always slower to access a global variable than it is to access a local variable, because the scope chain must be traversed. Anything you can do to decrease the amount of time spent traversing the scope chain will increase overall script performance.
Perhaps the most important thing you can do to improve the performance of your scripts is to be wary of global lookups. Global variables and functions are always more expensive to use than local ones because they involve a scope chain lookup. Consider the following function:
function updateUI() {
let imgs = document.getElementsByTagName("img");
for (let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = '${document.title} image ${i}';
}
let msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
This function may look perfectly fine, but it has three references to the global document
object. If there are multiple images on the page, the document
reference in the for
loop could get executed dozens or hundreds of times, each time requiring a scope chain lookup. By creating a local variable that points to the document
object, you can increase the performance of this function by limiting the number of global lookups to just one:
function updateUI() {
let doc = document;
let imgs = doc.getElementsByTagName("img");
for (let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = '${doc.title} image ${i}';
}
let msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
Here, the document
object is first stored in the local doc
variable. The doc
variable is then used in place of document
throughout the rest of the code. There's only one global lookup in this function, compared to the previous version, ensuring that it will run faster.
A good rule of thumb is to store any global object that is used more than once in a function as a local variable.
The with
statement should be avoided where performance is important. Similar to functions, the with
statement creates its own scope and therefore increases the length of the scope chain for code executed within it. Code executed within a with
statement is guaranteed to run slower than code executing outside because of the extra steps in the scope chain lookup.
It is rare that the with
statement is required because it is mostly used to eliminate extra characters. In most cases, a local variable can be used to accomplish the same thing without introducing a new scope. Here is an example:
function updateBody() {
with(document.body) {
console.log(tagName);
innerHTML = "Hello world!";
}
}
The with
statement in this code enables you to use document.body
more easily. The same effect can be achieved by using a local variable, as follows:
function updateBody() {
let body = document.body;
console.log(body.tagName);
body.innerHTML = "Hello world!";
}
Although this code is slightly longer, it reads better than the with
statement, ensuring that you know the object to which tagName
and innerHTML
belong. This code also saves global lookups by storing document.body
in a local variable.
As with other languages, part of the performance equation has to do with the algorithm or approach used to solve the problem. Skilled developers know from experience which approaches are likely to achieve better performance results. Many of the techniques and approaches that are typically used in other programming languages can also be used in JavaScript.
In computer science, the complexity of algorithms is represented using O notation. The simplest, and fastest, algorithm is a constant value or O(1). After that, the algorithms just get more complex and take longer to execute. The following table lists the common types of algorithms found in JavaScript.
NOTATION | NAME | DESCRIPTION |
O(1) | Constant | Amount of time to execute remains constant no matter the number of values. Represents simple values and values stored in variables. |
O(log n) | Logarithmic | Amount of time to execute is related to the number of values, but each value need not be retrieved for the algorithm to complete. Example: binary search. |
O(n) | Linear | Amount of time to execute is directly related to the number of values. Example: iterating over all items in an array. |
O(n^2) | Quadratic | Amount of time to execute is related to the number of values such that each value must be retrieved at least n times. Example: insertion sort. |
Constant values, or O(1), refer to both literals and values that are stored in variables. The notation O(1) indicates that the amount of time necessary to retrieve a constant value remains the same regardless of the number of values. Retrieving a constant value is an extremely efficient process and so is quite fast. Consider the following:
let value = 5;
let sum = 10 + value;
console.log(sum);
This code performs four constant value lookups: the number 5, the variable value
, the number 10, and the variable sum
. The overall complexity of this code is then considered to be O(1).
Accessing array items is also an O(1) operation in JavaScript, performing just as well as a simple variable lookup. So the following code is just as efficient as the previous example:
let values = [5, 10];
let sum = values[0] + values[1];
console.log(sum);
Using variables and arrays is more efficient than accessing properties on objects, which is an O(n) operation. Every property lookup on an object takes longer than accessing a variable or array, because a search must be done for a property of that name up the prototype chain. Put simply, the more property lookups there are, the slower the execution time. Consider the following:
let values = { first: 5, second: 10 };
let sum = values.first + values.second;
console.log(sum);
This code uses two property lookups to calculate the value of sum
. Doing one or two property lookups may not result in significant performance issues, but doing hundreds or thousands will definitely slow down execution.
Be wary of multiple property lookups to retrieve a single value. For example, consider the following:
let query = window.location.href.substring(window.location.href.indexOf("?"));
In this code, there are six property lookups: three for window.location.href.substring()
and three for window.location.href.indexOf()
. You can easily identify property lookups by counting the number of dots in the code. This code is especially inefficient because the window.location.href
value is being used twice, so the same lookup is done twice.
Whenever an object
property is being used more than once, store it in a local variable. You'll still take the initial O(n) hit to access the value the first time, but every subsequent access will be O(1), which more than makes up for it. For example, the previous code can be rewritten as follows:
let url = window.location.href;
let query = url.substring(url.indexOf("?"));
This version of the code has only four property lookups, a savings of 33 percent over the original. Making this kind of optimization in a large script is likely to lead to larger gains.
Generally speaking, any time you can decrease the complexity of an algorithm, you should replace as many property lookups as possible by using local variables to store the values. Furthermore, if you have an option to access something as a numeric array position or a named property (such as with NodeList
objects), use the numeric position.
Loops are one of the most common constructs in programming and, as such, are found frequently in JavaScript. Optimizing these loops is an important part of the performance optimization process because they run the same code repeatedly, automatically increasing execution time. There's been a great deal of research done into loop optimization for other languages, and these techniques also apply to JavaScript. The basic optimization steps for a loop are as follows:
for
and while
, both of which are pretest loops. Posttest loops, such as do-while
, avoid the initial evaluation of the terminal condition and tend to run faster.
These changes are best illustrated with an example. The following is a basic for
loop:
for (let i = 0; i < values.length; i++) {
process(values[i]);
}
This code increments the variable i
from 0 up to the total number of items in the values
array. Assuming that the order in which the values are processed is irrelevant, the loop can be changed to decrement i
instead, as follows:
for (let i = values.length - 1; i >= 0; i--) {
process(values[i]);
}
Here, the variable i
is decremented each time through the loop. In the process, the terminal condition is simplified by removing the O(n) call to values.length
and replacing it with the O(1) call of 0. Because the loop body has only a single statement, it can't be optimized further. However, the loop itself can be changed into a posttest loop like this:
let i = values.length-1;
if (i > -1) {
do {
process(values[i]);
}while(--i >= 0);
}
The primary optimization here is combining the terminal condition and the decrement operator into a single statement. At this point, any further optimization would have to be done to the process()
function itself because the loop is fully optimized.
Keep in mind that using a posttest loop works only when you're certain that there will always be at least one value to process. An empty array causes an unnecessary trip through the loop that a pre-test loop would otherwise avoid.
When the number of times through a loop is finite, it is often faster to eliminate the loop altogether and replace it with multiple function calls. Consider the loop from the previous example. If the length of the array will always be the same, it may be more optimal to simply call process()
on each item, as in the following code:
// eliminated the loop
process(values[0]);
process(values[1]);
process(values[2]);
This example assumes that there are only three items in the values
array and simply calls process()
directly on each item. Unrolling loops in this way eliminates the overhead of setting up a loop and processing a terminal condition, making the code run faster.
If the number of iterations through the loop can't be determined ahead of time, you may want to consider using a technique called Duff's device. The technique is named after its creator, Tom Duff, who first proposed using it in the C programming language. Jeff Greenberg is credited with implementing Duff's device in JavaScript. The basic idea of Duff's device is to unroll a loop into a series of statements by calculating the number of iterations as a multiple of 8. Consider the following code example:
// credit: Jeff Greenberg for JS implementation of Duff's Device
// assumes values.length> 0
let iterations = Math.ceil(values.length / 8);
let startAt = values.length % 8;
let i = 0;
do {
switch(startAt) {
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt = 0;
} while (--iterations> 0);
This implementation of Duff's device starts by calculating how many iterations through the loop need to take place by dividing the total number of items in the values
array by 8
. The ceiling function is then used to ensure that the result is a whole number. The startAt
variable holds the number of items that wouldn't be processed if the iterations were based solely on dividing by 8
. When the loop executes for the first time, the startAt
variable is checked to see how many extra calls should be made. For instance, if there are 10 values in the array, startAt
would be equal to 2
, so process()
would be called only twice the first time through the loop. At the bottom of the loop, startAt
is reset to 0
so that each subsequent time through the loop results in eight calls to process()
. This unrolling speeds up processing of large data sets.
The book Speed Up Your Site by Andrew B. King (New Riders, 2003) proposed an even faster Duff's device technique that separated the do-while
loop into two separate loops. Here's an example:
// credit: Speed Up Your Site (New Riders, 2003)
let iterations = Math.floor(values.length / 8);
let leftover = values.length % 8;
let i = 0;
if (leftover> 0) {
do {
process(values[i++]);
} while (--leftover> 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations> 0);
In this implementation, the leftover count that wouldn't have been handled in the loop when simply dividing by 8
is handled in an initial loop. Once those extra items are processed, execution continues in the main loop that calls process()
eight times. This approach is almost 40 percent faster than the original Duff's device implementation.
Unrolling loops can yield big savings for large data sets but may not be worth the extra effort for small data sets. The trade-off is that it takes more code to accomplish the same task, which is typically not worth it when large data sets aren't being processed.
Double interpretation penalties exist when JavaScript code tries to interpret JavaScript code. This situation arises when using the eval()
function or the Function
constructor or when using setTimeout()
with a string argument. Here are some examples:
// evaluate some code - AVOID!!
eval("console.log('Hello world!')");
// create a new function - AVOID!!
let sayHi = new Function("console.log('Hello world!')");
// set a timeout - AVOID!!
setTimeout("console.log('Hello world!')", 500);
In each of these instances, a string containing JavaScript code has to be interpreted. This can't be done during the initial parsing phase because the code is contained in a string, which means a new parser has to be started while the JavaScript code is running to parse the new code. Instantiating a new parser has considerable overhead, so the code runs slower than if it were included natively.
There are workarounds for all of these instances. It's rare that eval()
is absolutely necessary, so try to avoid it whenever possible. In this case, the code could just be included inline. For the Function
constructor, the code can be rewritten as a regular function quite easily, and the setTimeout()
call can pass in a function as the first argument. Here are some examples:
// fixed
console.log('Hello world!');
// create a new function - fixed
let sayHi = function() {
console.log('Hello world!');
};
// set a timeout - fixed
setTimeout(function() {
console.log('Hello world!');
}, 500);
To increase the performance of your code, avoid using strings that need to be interpreted as JavaScript whenever possible.
There are a few other things to consider when evaluating the performance of your script. The following aren't major issues, but they can make a difference when used frequently:
if-else
statements, converting it to a single switch statement can result in faster code. You can further improve the performance of switch statements by organizing the cases in the order of most likely to least likely.The number of statements in JavaScript code affects the speed with which the operations are performed. A single statement can complete multiple operations faster than multiple statements each performing a single operation. The task, then, is to seek out statements that can be combined in order to decrease the execution time of the overall script. To do so, you can look for several patterns.
One area in which developers tend to create too many statements is in the declaration of multiple variables. It's quite common to see code declaring multiple variables using multiple let
statements, such as the following:
// four statements - wasteful
let count = 5;
let color = "blue";
let values = [1,2,3];
let now = new Date();
In strongly typed languages, variables of different data types must be declared in separate statements. In JavaScript, however, all variables can be declared using a single let
statement. The preceding code can be rewritten as follows:
// one statement
let count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
Here, the variable declarations use a single let
statement and are separated by commas. This is an optimization that is easy to make in most cases and performs much faster than declaring each variable separately.
Any time you are using an iterative value (that is, a value that is being incremented or decremented at various locations), combine statements whenever possible. Consider the following code snippet:
let name = values[i];
i++;
Each of the two preceding statements has a single purpose: the first retrieves a value from values
and stores it in name
; the second increments the variable i
. These can be combined into a single statement by inserting the iterative value into the first statement, as shown here:
let name = values[i++];
This single statement accomplishes the same thing as the previous two statements. Because the increment operator is postfix, the value of i
isn't incremented until after the rest of the statement executes. Whenever you have a similar situation, try to insert the iterative value into the last statement that uses it.
Throughout this book, you've seen two ways of creating arrays and objects: using a constructor or using a literal. Using constructors always leads to more statements than are necessary to insert items or define properties, whereas literals complete all operations in a single statement. Consider the following example:
// four statements to create and initialize array - wasteful
let values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
// four statements to create and initialize object - wasteful
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function() {
console.log(this.name);
};
In this code, an array and an object are created and initialized. Each requires four statements: one to call the constructor and three to assign data. These can easily be converted to use literals as follows:
// one statement to create and initialize array
let values = [123, 456, 789];
// one statement to create and initialize object
let person = {
name: "Nicholas",
age: 29,
sayName() {
console.log(this.name);
}
};
This rewritten code contains only two statements: one to create and initialize the array, and one to create and initialize the object. What previously took eight statements now takes only two, reducing the statement count by 75 percent. The value of these optimizations is even greater in codebases that contain thousands of lines of JavaScript.
Whenever possible, replace your array and object declarations with their literal representation to eliminate unnecessary statements.
Of all the parts of JavaScript, the DOM is without a doubt the slowest part. DOM manipulations and interactions take a large amount of time because they often require rerendering all or part of the page. Furthermore, seemingly trivial operations can take longer to execute because the DOM manages so much information. Understanding how to optimize interactions with the DOM can greatly increase the speed with which scripts complete.
Whenever you access part of the DOM that is part of the displayed page, you are performing a live update. Live updates are so called because they involve immediate (live) updates of the page's display to the user. Every change, whether it be inserting a single character or removing an entire section, incurs a performance penalty as the browser recalculates thousands of measurements to perform the update. The more live updates you perform, the longer it will take for the code to completely execute. The fewer live updates necessary to complete an operation, the faster the code will be. Consider the following example:
let list = document.getElementById("myList"),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode('Item ${i}');
}
This code adds ten items to a list. For each item that is added, there are two live updates: one to add the <li>
element and another to add the text node to it. Because ten items are being added, that's a total of twenty live updates to complete this operation.
To fix this performance bottleneck, you need to reduce the number of live updates. There are generally two approaches to this. The first is to remove the list from the page, perform the updates, and then reinsert the list into the same position. This approach is not ideal because it can cause unnecessary flickering as the page updates each time. The second approach is to use a document fragment to build up the DOM structure and then add it to the list
element. This approach avoids live updates and page flickering. Consider the following:
let list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
There is only one live update in this example, and it occurs after all items have been created. The document fragment is used as a temporary placeholder for the newly created items. All items are then added to the list, using appendChild()
. Remember, when a document fragment is passed in to appendChild()
, all of the children of the fragment are appended to the parent, but the fragment itself is never added.
Whenever updates to the DOM are necessary, consider using a document fragment to build up the DOM structure before adding it to the live document.
There are two ways to create new DOM nodes on the page: using DOM methods such as createElement()
and appendChild()
, and using innerHTML
. For small DOM changes, the two techniques perform roughly the same. For large DOM changes, however, using innerHTML
is much faster than creating the same DOM structure using standard DOM methods.
When innerHTML
is set to a value, an HTML parser is created behind the scenes, and the DOM structure is created using the native DOM calls rather than JavaScript-based DOM calls. The native methods execute much faster because they are compiled rather than interpreted. The previous example can be rewritten to use innerHTML
like this:
let list = document.getElementById("myList"),
html = "";
for (let i = 0; i < 10; i++) {
html += '<li>Item ${i}</li>';
}
list.innerHTML = html;
This code constructs an HTML string and then assigns it to list.innerHTML
, which creates the appropriate DOM structure. Although there is always a small performance hit for string concatenation, this technique still performs faster than performing multiple DOM manipulations.
The key to using innerHTML
, as with other DOM operations, is to minimize the number of times it is called. For instance, the following code uses innerHTML
too much for this operation:
let list = document.getElementById("myList");
for (let i = 0; i < 10; i++) {
list.innerHTML += '<li>Item ${i}</li>'; // AVOID!!!
}
The problem with this code is that innerHTML
is called each time through the loop, which is incredibly inefficient. A call to innerHTML
is, in fact, a live update and should be treated as such. It's far faster to build up a string and call innerHTML
once than it is to call innerHTML
multiple times.
Most web applications make extensive use of event handlers for user interaction. There is a direct relationship between the number of event handlers on a page and the speed with which the page responds to user interaction. To mitigate these penalties, you should use event delegation whenever possible.
Event delegation takes advantage of events that bubble. Any event that bubbles can be handled not just at the event target but also at any of the target's ancestors. Using this knowledge, you can attach event handlers at a high level that are responsible for handling events for multiple targets. Whenever possible, attach an event handler at the document level that can handle events for the entire page.
The pitfalls of HTMLCollection
objects have been discussed throughout this book because they are a big performance sink for web applications. Keep in mind that any time you access an HTMLCollection
, whether it be a property or a method, you are performing a query on the document, and that querying is quite expensive. Minimizing the number of times you access an HTMLCollection
can greatly improve the performance of a script.
Perhaps the most important area in which to optimize HTMLCollection
access is loops. Moving the length calculation into the initialization portion of a for
loop was discussed previously. Now consider this example:
let images = document.getElementsByTagName("img");
for (let i = 0, len = images.length; i < len; i++) {
// process
}
The key here is that the length
is stored in the len
variable instead of constantly accessing the length
property of the HTMLCollection
. When using an HTMLCollection
in a loop, you should make your next step a retrieval of a reference to the item you'll be using, as shown here, in order to avoid calling the HTMLCollection
multiple times in the loop body:
let images = document.getElementsByTagName("img"),
image;
for (let i = 0, len=images.length; i < len; i++) {
image = images[i];
// process
}
This code adds the image
variable, which stores the current image. Once this is complete, there should be no further reason to access the images HTMLCollection
inside the loop.
When writing JavaScript, it's important to realize when HTMLCollection
objects are being returned so you can minimize accessing them. An HTMLCollection
object is returned when any of the following occurs:
getElementsByTagName()
is made.childNodes
property of an element is retrieved.attributes
property of an element is retrieved.document.forms
, document.images
, and so forth.Understanding when you're using HTMLCollection
objects and making sure you're using them appropriately can greatly speed up code execution.
Perhaps the most important part of any JavaScript solution is the final deployment to the website or web application in production. You've done a lot of work before this point, architecting and optimizing a solution for general consumption. It's time to move out of the development environment and into the web, where real users can interact with it. Before you do so, however, there are a number of issues that need to be addressed.
One of the most important things you can do to ready JavaScript code for deployment is to develop some type of build process around it. The typical pattern for developing software is write-compile-test, in that you write the code, compile it, and then run it to ensure that it works. Because JavaScript is not a compiled language, the pattern often becomes write-test, where the code you write is the same code you test in the browser. The problem with this approach is that it's not optimal; the code you write should not be passed, untouched, to the browser, for the following reasons:
For these reasons, it's best to define a build process for your JavaScript files.
A build process starts by defining a logical structure for storing your files in source control. It's best to avoid having a single file that contains all of your JavaScript. Instead, follow the pattern that is typically taken in object-oriented languages: separate each object or custom type into its own file. Doing so ensures that each file contains just the minimum amount of code, making it easier to make changes without introducing errors. Additionally, in environments that use concurrent source control systems such as Git, CVS, or Subversion, this reduces the risk of conflicts during merge operations.
Keep in mind that separating your code into multiple files is for maintainability and not for deployment. For deployment, you'll want to combine the source files into one or more rollup files. It's recommended that web applications use the smallest number of JavaScript files possible, because HTTP requests are some of the main performance bottlenecks on the web. Keep in mind that including a JavaScript file via a vanilla <script>
tag is a blocking operation that stops all other downloads while the code is downloaded and executed. Therefore, try to logically group JavaScript code into deployment files.
If you're putting together an application that's anything more than a few files, you will likely find yourself reaching for a task runner to automate tasks for you. The task runner can perform jobs such as linting, bundling, transpilation, starting a local server, deployment, or any other scripted program.
Much of the time, jobs that your task runner will perform are available through command-line interfaces, and therefore your task runner will merely be a tool that aids in grouping and ordering complex command line invocations. In this sense, a task runner in many ways is very similar to a .bashrc
file. In other cases, the tools you wish to use in automated tasks will have designated plugins compatible
If you're using NodeJS and npm to package your JavaScript assets, two popular task runners are Grunt (www.gruntjs.com
) and Gulp (www.gulpjs.com
). Both of these tools are robust task runners whose jobs and instructions are defined inside configuration files written in plain JavaScript. The benefit of using these task runners is that each enjoys an ecosystem of plugins, which allow the tools to directly interface with npm packages. Details of these plugins can be found in the appendixes.
An increasingly common and extremely effective strategy for reducing payload size is tree shaking. As mentioned in the Modules chapter, using a static module declaration style means that build tools can determine which parts of the codebase depend on other parts. More importantly, tree shaking is also capable of determining which parts of the codebase are not needed at all.
Build tools that implement tree shaking acknowledge that module imports are frequently selective and that entire segments of module files can be ignored in the final bundled file. Suppose this is your example application:
import { foo } from './utils.js';
console.log(foo);
export const foo = 'foo';
export const bar = 'bar'; // unused
Here, the bar
export is never used, and static analysis by a build tool can easily determine this is the case. When performing tree shaking, the build tool will completely strip out the bar
export from the bundled file. Static analysis also means that the build tool can determine dependencies that are unused and decline to include those as well. By performing tree shaking, the file size savings of the eventual bundle can be enormous.
Just because your codebase is written in modules doesn't mean that it should necessarily be served as modules. Often, JavaScript codebases composed of a large collection of modules will be bundled together at build time and served as one or a few different JavaScript files.
The module bundler's job is to identify the landscape of JavaScript dependencies involved in an application, combine them into a monolithic application, make informed decisions about how the modules should be serially organized and concatenated, and generate the output files that will be provided to the browser.
There is an abundance of build tooling that allows you to accomplish such a feat. Webpack, Rollup, and Browserify are just a few of the many options you have to convert a module-based codebase into a universally compatible page script.
Even though IDEs that understand and support JavaScript are starting to appear, most developers still check their syntax by running code in a browser. There are a couple of problems with this approach. First, this validation can't be easily automated or ported from system to system. Second, aside from syntax errors, problems are encountered only when code is executed, leaving it possible for errors to occur. Several tools are available to help identify potential issues with JavaScript code, the most popular being Douglas Crockford's JSLint (www.jslint.com
) and ESLint (www.eslint.org
).
Linters look for syntax errors and common coding errors in JavaScript code. Some of the potential issues they surface are as follows:
eval()
with
Adding code validation to your development cycle helps to avoid errors down the road. It's recommended that developers add some type of code validation to the build process as a way of identifying potential issues before they become errors.
When talking about JavaScript file compression, you're really talking about two things: code size and wire weight. Code size refers to the number of bytes that need to be parsed by the browser, and wire weight refers to the number of bytes that are actually transmitted from the server to the browser. In the early days of web development, these two numbers were almost always identical because source files were transmitted, unchanged, from server to client. In today's web, however, the two are rarely equal and realistically should never be.
Because JavaScript isn't compiled into byte code and is transmitted as source code, the source code files usually contain additional information and formatting that have no effect on the browser's JavaScript interpreter. A JavaScript minifier will perform transformations on your source code to make the file size as small as possible while retaining identical program flow.
Comments, extra white space, and long variable or function names improve readability for developers but are unnecessary extra bytes when sent to the browser. A minifier can decrease the file size by performing the following duties:
All JavaScript files should be minified with a minification tool before being deployed to a production environment. Adding a step in your build process to compress JavaScript files is an easy way to ensure that this always happens.
Similar in spirit to minification, code compilation generally refers to the process of taking source code and converting it into a form that is behaviorally identical but uses fewer bytes of JavaScript. This is distinct from minification in that the post-compilation code structure may be different, but it will still exhibit the same behavior as your original source code. Compilers are able to do this by ingesting the entirety of your JavaScript code and performing robust analysis on program flow.
Compilation might perform some of the following operations:
The code in your project repository will almost never be the exact code that will execute in your browser. ES6, ES7, and ES8 all introduce wonderful abilities into the ECMAScript specification, but different browsers will fully implement each of their features at different paces.
Using transpilation will allow you to wield all the newest syntactical specification features without having to worry about backwards browser compatibility. You can transpile your modern code to an older ECMAScript version—typically ES3 or ES5, depending on your needs—so that your code can work everywhere. Transpilation tools are covered in the appendixes.
Wire weight refers to the actual number of bytes sent from the server to the browser. The number of bytes doesn't necessarily have to be the same as the code size, because of the compression capabilities of both the server and the browser. All of the five major web browsers—Internet Explorer/Edge, Firefox, Safari, Chrome, and Opera—support client-side decompression of resources that they receive. The server is therefore able to compress JavaScript files using server-dependent capabilities. As part of the server response, a header is included indicating that the file has been compressed using a given format. The browser then looks at the header to determine that the file is compressed, and then decompresses it using the appropriate format. The result is that the amount of bytes transferred over the network is significantly less than the original code size.
For example, using two modules available for the Apache web server (mod_gzip
and mod_deflate
) results in savings of around 70 percent of the original file size of JavaScript files. This is largely due to the fact that JavaScript files are plain text and can therefore be compressed very efficiently. Decreasing the wire weight of your files decreases the amount of time it takes to transmit to the browser. Keep in mind that there is a slight trade-off because the server must spend time compressing the files on each request, and the browser must take some time to decompress the files once they arrive. Generally speaking, however, the trade-off is well worth it.
As JavaScript development has matured, best practices have emerged. What once was considered a hobby is now a legitimate profession and, as such, has experienced the type of research into maintainability, performance, and deployment traditionally done for other programming languages.
Maintainability in JavaScript has to do partially with the following code conventions:
As the amount of JavaScript has increased in web applications, performance has become more important. Therefore, you should keep these things in mind:
if
.The last step in the process is deployment. Here are some key points discussed in this chapter: