DEV Community

loading...
Cover image for Optimising JavaScript code

Optimising JavaScript code

nwamugo profile image UGOJI Duziem ・6 min read

An optimized piece of code is any code that works optimally, i.e. code that is efficient. We say something is efficient, when that thing does not waste time or effort or expense (expense includes computer memory). The reward for an optimised JavaScript code is generally a less buggy, smaller-sized, smoother and faster application.

In this article I reproduce a program, I wrote for a front-end web application to check if a number given by the user is a narcissistic number.

The flesh of the app is codified by the HTML and CSS given below.
<body>
  <div class="container">
    <h3>Narcissistic Number</h3>
    <p>Type in a number to check if it's narcissistic</p>
    <input type="number" id="inputField" onfocus="this.value=''" autofocus />
    <button id="btn">evaluate</button>
    <p id="displayResult"></p>
  </div>
</body>
Enter fullscreen mode Exit fullscreen mode
The CSS
.container {
  margin: 0 auto;
  width: 280px;
  background-color: #fafafa;
}

p[data-status="true"] {
  color: green;
}

p[data-status="false"] {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

The above HTML & CSS produce a beautiful application that looks like this
Narcissistic number checker

Now for the spirit of the app, (for the body cannot live without the mind - Morpheous), our JavaScript code that makes the app tick, is coded thus...

let displayResultEl = document.getElementById("displayResult");
let inputField = document.getElementById("inputField");

function isInteger(x) {
  return x % 1 === 0;
}

let narcissistic = function() {
  let strValue = inputField.value; //this is a string
  if(isInteger(strValue)) { 
    let power = strValue.length;
    let allDigits = [];

    for(let i = 0; i < power; i++) {
      allDigits.push(parseInt(strValue[i], 10));
    }

    let raisedToPowers = allDigits.map(digit => 
             Math.pow(digit,power));
    let total = raisedToPowers.reduce(
               (sum, raisedToPower) => {
                 return sum + raisedToPower;
                }, 0);
     if(total == strValue) {
       displayResultEl.dataset.status = "true";
       return `TRUE! ${strValue} is a narcissitic number`;
     } else {
       displayResultEl.dataset.status = "false";
       return `False! ${strValue} is not a narcissistic 
              number`;
     }
   } else {
       displayResultEl.dataset.status = "false";
       return "Use positive integers only";
   }
}

let btnEl = document.getElementById("btn");
btnEl.onclick = function() {
  displayResultEl.innerHTML = narcissistic();
}

const enterKey = 13;
inputField.addEventListener("keyup", function(event) {
  event.preventDefault();
  if(event.keyCode === enterKey) {
     btnEl.click();
  }
});
Enter fullscreen mode Exit fullscreen mode

Summarily, what the above JavaScript code accomplishes is,

  • It takes the input typed in by the user, and checks to see if it is a narcissistic number or not. It displays the result of that check.

Hurray! The app works🙈😀. By the way, narcissistic numbers include, in addition to all single digit numbers, 153, 370, 371, 407, 1634 etc.

Back to our app, the check is started either when the user hits the button on the app, or after the user has pressed the enter key on their keyboard. A magnificent triumph!
However, when you have gotten your app to do what it is intended to do, you then want to optimise for performance and maintainability. As it is, the JavaScript solution above, as most first time code solutions, is clunky and not optimised. Bugs delight in such code.


The Refactor

So what's with the above JavaScript code, and where can we optimise?
When we observe the code, we notice a couple of things

  1. There are variables in the global space.
    Global variables make codes harder to maintain, as they could be used anywhere.

  2. There is a callback function using a variable (btnEl) outside its scope.
    This is a major gotcha for developers. Because of the concept of closure, reference to a variable declared outside its scope remains. This is a cause of Memory leak, which can lead to all types of nightmares as the application gets bigger.

  3. Objects declared and initialized in one outer scope are being brought into the inner local scope wholly, when perhaps the inner scope only needs one property or two. An object being used in this manner only adds up more memory usage. A destructured object allows for inner local scopes to use just those properties they need without having to bring in all kb of that object. For instance, in our code, the narcissistic function has inputField.value inside its scope. In reality, that function holds all the properties in inputField, not just value. This is unnecessary memory consumption.

  4. There may be redundant lines of code, which only increases the time for the algorithm to run

  5. The narcissistic function does more than one thing. It checks for the narcissistic status of the input, which is what it is set up to do. But then goes on as well to update DOM elements (a secong thing). These concerns can be separated.

  6. There is no clear pattern or definite architecture to our code. It seems anything can be anywhere.

The first step towards refactoring, and therefore optimisation of code, is observation, which is what we have done. Let us see if we can apply some improvement.


The Improvement

Picking it from (6), every code needs a discernible structure. You may call it pattern or architecture. Any name is fine by me as long as it brings in a bit of order. Let me also say, there is no one structure to rule them all. For the code above, I will like to use a module pattern, which I grasped while taking a Jonas Schmedtmann course on JavaScript.
In essence, every front-end application has its UI part (UI module), its computational part (Data Module), and its controller part (App Controller Module).

  • Anything directly affecting the UI, stays inside the UI module.
  • The calculations, permutations, brain work, stays inside the Data Module.
  • Finally the App Controller module takes care of all event handlers, as well as acts as the intermediary between the UI and the Data modules.
This separation of concerns is captured thus...
//UI Controller
let UIController = (function() {

  return {
    ...
  }
})();


//Data Controller
let dataController = (function(){

  return {
    ...
  }
})();


// App controller
let controller = (function(dataCtrl, UICtrl) {
  return {
    init: function() {
      console.log('Application has started');
      setupEventListeners();
    }
  }
})(dataController, UIController);

controller.init();
Enter fullscreen mode Exit fullscreen mode

You can see now, with a structure, we have solved many things at once. We will not have variables lying about in the global space anymore, they will have to fit in, in one of the module's local scopes. This clarity gives every developer confidence that they are not altering what they needn't alter.
After this improvement, you want to improve the code itself, its algorithm, remove redundant lines and also ensure that functions do only one thing. Let us see what our final code looks like...

//UI Controller
let UIController = (function() {
  let DOMstrings = {
    displayResult: "displayResult",
    inputField: "inputField",
    btn: "btn"
  }

  let outputStatement = function(resultObj) {
    let {isNarcissistic, strValue, exponent, sum} = resultObj;
    let sentence = function(value) {
      return `${strValue} is ${value ? '' : 'not'} a narcissistic value.\n
      The sum of its own digits, each raised to the total digits count ${exponent}, is ${sum}`
    };

    switch(isNarcissistic) {
      case false:
        return `No, ${sentence(false)}`;
      case true:
        return `Yes, ${sentence(true)}`;
      default:
        return "Please type in an integer"
    }
  }

  return {
    getDOMstrings: function() {
      return DOMstrings;
    },
    getOutputStatement: function(value) {
      return outputStatement(value);
    }
  }
})();



//Data Controller
let dataController = (function(){
  let numValue;

  let validateInput = function(strValue) {
    numValue = parseInt(strValue, 10);
    return isNaN(numValue) ? false : true;
  }

  let narcissistic = function(strValue) {
    if (strValue != numValue) {
      return { isNarcissistic: false };
    }
    let base = strValue < 0 ? -1 : 1;
    let length = strValue.length;
    let exponent = strValue < 0 ? length - 1 : length;
    let start = strValue < 0 ? 1 : 0;
    let sum = 0;

    for (let i = start; i < length; i++) {
      sum += Math.pow(strValue[i], exponent)
    }

    let signedInteger = base * sum;

    return {
      isNarcissistic: (signedInteger == strValue),
      sum: signedInteger,
      exponent,
      strValue
    };
  }

  return {
    checkValidInput: function(input) {
      return validateInput(input);
    },

    checkNarcissistic: function(strValue) {
      return narcissistic(strValue);
    }
  }
})();



// App controller
let controller = (function(dataCtrl, UICtrl) {
  let { inputField, btn, displayResult } = UICtrl.getDOMstrings();
  let { getOutputStatement } = UICtrl;
  let { checkValidInput, checkNarcissistic } = dataCtrl;
  let inputFieldEl = document.getElementById(inputField);

  let setupEventListeners = function() {
    let btnEl = document.getElementById(btn);

    inputFieldEl.addEventListener("keyup", keyAction);
    btnEl.addEventListener("click", executeInput);
  }

  let keyAction = function(event) {
      event.preventDefault();
      const enterKey = 13;
      if (event.keyCode === enterKey || event.which === enterKey) executeInput();
  }

  let executeInput = function() {
    let strValue = inputFieldEl.value;
    let isValidInput = checkValidInput(strValue);
    let displayResultEl = document.getElementById(displayResult);
    if (isValidInput) {
      let result = checkNarcissistic(strValue);
      displayResultEl.dataset.status = result.isNarcissistic ? "true" : "false";
      displayResultEl.innerHTML = getOutputStatement(result);
    } else {
      displayResultEl.dataset.status = "false";
      displayResultEl.innerHTML = getOutputStatement('NaN');
    }
  }

  return {
    init: function() {
      console.log('Application has started');
      setupEventListeners();
    }
  }
})(dataController, UIController);

controller.init();
Enter fullscreen mode Exit fullscreen mode

Observe our refactored code above...

  1. It consumes less memory, as objects are now destructured, and functions use the property they want without having to hold the whole weight of that object.
  2. It runs faster than our old code and even takes care of more edge cases, as refactoring exposed some bugs not previously seen.
  3. There is no fear of a DOM leak (which severely hampers apps). Our rewritten callbacks do not refer to any variable outside its scope. Therefore when the callback function is done, JavaScript cleans up the memory without any reference left behind(closure).
  4. Each function in the code does only one thing, and concerns are properly separated. Unlike the old code, now the narcissistic function only checks if it's narcissistic, and another function has the responsibility of updating the DOM. Everything is well spelt out.
  5. Lastly, it's beautiful to read.

I do think it's beautiful to read. Thank you dear reader for coming with me through this journey. Together we have seen a code transform from Gandalf the Grey to Gandalf the White🧙. Your thoughts are very much welcome. And remember, if you have ever whispered under your breath, "For Frodo", think of me as family.🤗

You can see the full working application here
https://codepen.io/Duz/pen/oaGdmG

Discussion (0)

Forem Open with the Forem app