Is Your Code Slow?: Avoid These 19 Common JavaScript and Node.js Mistakes

Written by jsdevjournal | Published Invalid Date
Tech Story Tags: javascript | nodejs | web-development | web-performance | javascript-mistakes | nodejs-mistakes | coding-mistakes | optimizing-slow-code

TLDRvia the TL;DR App

Speed. Performance. Responsiveness. These ideals reign supreme in the world of web development, especially JavaScript and Nodejs. A slow or janky website is the hallmark of an amateur, while a slick, optimized experience delights users and sets professionals apart.

But creating truly high-performance web apps is littered with pitfalls. Mistakes abound that can drag down the pace of your JavaScript without you even realizing it. Tiny oversights that bloat your code and surreptitiously sap speed away bit by bit.

You vigilantly minify your code and leverage caching...yet your site still feels oddly sluggish at times. The UI stutters during scrolling or when buttons are clicked. Pages take eons to load.

What's going on?

Turns out, there are many common ways we inadvertently slow down our JavaScript. Anti-patterns that, over time, can hobble site performance.

The good news? These mistakes can be avoided.

Today we're spotlighting the top 19 performance pitfalls that can secretly slow down JavaScript and Node.js apps. We'll explore what causes them through illustrative examples and actionable solutions to optimize your code.

Identifying and eliminating these hazards is key to crafting buttery smooth web experiences that delight users. So, let's dive in!

1. Improper Variable Declarations and Scope

When first learning JavaScript, it's tempting to declare all variables globally. However, this causes problems down the road. Let's look at an example:

// globals.js

var color = 'blue';

function printColor() {
  console.log(color); 
}

printColor(); // Prints 'blue'

This works fine, but imagine if we loaded another script:

// script2.js

var color = 'red';

printColor(); // Prints 'red'!

Because color is global, script2.js overwritten it! To fix this, declare variables locally inside functions whenever possible:

function printColor() {
  var color = 'blue'; // local variable
  
  console.log(color);
}

printColor(); // Prints 'blue'

Now, changes in other scripts won't affect printColor.

Declaring variables in the global scope when unnecessary is an anti-pattern. Try to limit globals to configuration constants. For other variables, declare them locally in the smallest possible scope.

2. Inefficient DOM Manipulation

When updating DOM elements, batch changes instead of manipulating one node at a time. Consider this example:

const ul = document.getElementById('list');

for (let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  
  ul.appendChild(li);
}

This appends list items one by one. It is better to build a string first then set .innerHTML:

const ul = document.getElementById('list');
let html = '';

for (let i = 0; i < 10; i++) {
  html += `<li>${i}</li>`; 
}

ul.innerHTML = html;

Building a string minimizes reflows. We update the DOM once instead of 10 times.

For multiple updates, build up changes, then apply at the end. Or better yet, use DocumentFragment to batch appends.

3. Excessive DOM Manipulation

Frequent DOM updates can crush performance. Consider a chat app that inserts messages into the page.

Bad:

// New message received
const msg = `<div>${messageText}</div>`;
chatLog.insertAdjacentHTML('beforeend', msg);

This naively inserts on each message. Better to throttle updates:

Good:

let chatLogHTML = '';
const throttleTime = 100; // ms

// New message received  
chatLogHTML += `<div>${messageText}</div>`;

// Throttle DOM updates
setTimeout(() => {
  chatLog.innerHTML = chatLogHTML;
  chatLogHTML = ''; 
}, throttleTime);

Now, we update at most every 100ms, keeping DOM operations low.

For highly dynamic UIs, consider virtual DOM libraries like React. These minimize DOM manipulation using a virtual representation.

4. Lack of Event Delegation

Attaching event listeners to many elements creates needless overhead. Consider a table with delete buttons on each row:

Bad:

const rows = document.querySelectorAll('table tr');

rows.forEach(row => {
  const deleteBtn = row.querySelector('.delete');  

  deleteBtn.addEventListener('click', handleDelete);
});

This adds a listener to each delete button. Better to use event delegation:

Good:

const table = document.querySelector('table');

table.addEventListener('click', e => {
  if (e.target.classList.contains('delete')) {
    handleDelete(e);
  }
});

Now, there's a single listener on the <table>. Less memory overhead.

Event delegation utilizes event bubbling. One listener can handle events from multiple descendants. Use delegation whenever applicable.

5. Inefficient String Concatenation

When concatenating strings in a loop, performance suffers. Consider this code:

let html = '';

for (let i = 0; i < 10; i++) {
  html += '<div>' + i + '</div>';
}

Creating new strings requires memory allocation. Better to use an array:

const parts = [];

for (let i = 0; i < 10; i++) {
  parts.push('<div>', i, '</div>');
}

const html = parts.join('');

Building an array minimizes intermediate strings. .join() concatenates once at the end.

For multiple string additions, use array joining. Also, consider template literals for embedded values.

6. Unoptimized Loops

Loops often cause performance problems in JavaScript. A common mistake is repeatedly accessing array length:

Bad:

const items = [/*...*/];

for (let i = 0; i < items.length; i++) {
  // ...
}

Redundantly checking .length inhibits optimizations. Better:

Good:

const items = [/*...*/];  
const len = items.length;

for (let i = 0; i < len; i++) {
  // ...
} 

Caching length improves speed. Other optimizations include hoisting invariants out of loops, simplifying termination conditions, and avoiding expensive operations inside iterations.

7. Unnecessary Synchronous Operations

JavaScript's async capabilities are a key advantage. But beware of blocking I/O! For example:

Bad:

const data = fs.readFileSync('file.json'); // blocks!

This stalls execution while reading from the disk. Instead, use callbacks or promises:

Good:

fs.readFile('file.json', (err, data) => {
  // ...
});

Now, the event loop continues while the file is read. For complex flows, async/await simplifies asynchronous logic. Avoid synchronous operations to prevent blocking.

8. Blocking the Event Loop

JavaScript uses a single-threaded event loop. Blocking it stalls execution. Some common blockers:

  • Heavy computational tasks
  • Synchronous I/O
  • Unoptimized algorithms

For example:

function countPrimes(max) {
  // Unoptimized loop
  for (let i = 0; i <= max; i++) {
    // ...check if prime...
  }
}

countPrimes(1000000); // Long running!

This executes synchronously, blocking other events. To avoid:

  • Defer unnecessary work
  • Batch data processing
  • Use Worker threads
  • Look for optimization opportunities

Keep the event loop running smoothly. Profile periodically to catch blocking code.

9. Inefficient Error Handling

It's vital to handle errors properly in JavaScript. But beware of performance pitfalls!

Bad:

try {
  // ...
} catch (err) {
  console.error(err); // just logging
}

This captures errors but takes no corrective action. Unhandled errors often lead to memory leaks or data corruption.

Better:

try {
  // ...
} catch (err) {
  console.error(err);
  
  // Emit error event 
  emitError(err); 
  
  // Nullify variables
  obj = null;
  
  // Inform user
  showErrorNotice();
}

Logging isn't enough! Clean up artifacts, notify users, and consider recovery options. Use tools like Sentry to monitor errors in production. Handle all errors explicitly.

11. Memory Leaks

Memory leaks happen when memory is allocated but never released. Over time, leaks accumulate and degrade performance.

Common sources in JavaScript include:

  • Uncleaned-up event listeners
  • Outdated references to deleted DOM nodes
  • Cached data that's no longer needed
  • Accumulating state in closures

For example:

function processData() {
  const data = [];

  // Use closure to accumulate data
  return function() {
    data.push(getData()); 
  }
}

const processor = processData();

// Long running...keeps holding reference to growing data array!

The array keeps getting larger but is never cleared. To fix:

  • Use weak references
  • Clean up event listeners
  • Delete no-longer-needed references
  • Limit closured state size

Monitor memory usage and watch for growing trends. Proactively eliminate leaks before they pile up.

11. Overuse of Dependencies

While npm offers endless options, resist the urge to over-import! Each dependency increases bundle size and attack surface.

Bad:

import _ from 'lodash';
import moment from 'moment'; 
import validator from 'validator';
// etc...

Importing entire libraries for minor utilities. Better to cherries pick helpers as needed:

Good:

import cloneDeep from 'lodash/cloneDeep';
import { format } from 'date-fns';
import { isEmail } from 'validator'; 

Only import what you need. Review dependencies regularly to prune unused ones. Keep bundles lean and minimize dependencies.

12. Inadequate Caching

Caching allows skipping expensive computations by reusing prior results. But it's often overlooked.

Bad:

function generateReport() {
  // Perform expensive processing
  // to generate report data... 
}

generateReport(); // Computes
generateReport(); // Computes again!

Since inputs haven't changed, the report could be cached:

Good:

let cachedReport;

function generateReport() {
  if (cachedReport) {
    return cachedReport;
  }

  cachedReport = // expensive processing...
  return cachedReport; 
}

Now, repeated calls are fast. Other caching strategies:

  • Memory caches like Redis

  • HTTP caching headers

  • LocalStorage for client caching

  • CDNs for asset caching

Identify cache opportunities - they often provide big speedups!

13. Unoptimized Database Queries

When interfacing with databases, inefficient queries can bog down performance. Some issues to avoid:

Bad:

// No indexing
db.find({name: 'John', age: 35}); 

// Unecessary fields
db.find({first: 'John', last:'Doe', email:'[email protected]'}, {first: 1, last: 1});

// Too many separate queries
for (let id of ids) {
  const user = db.find({id});
}

This fails to utilize indexes, retrieves unused fields, and executes excessive queries.

Good:

// Use index on 'name' 
db.find({name: 'John'}).hint({name: 1});

// Only get 'email' field
db.find({first: 'John'}, {email: 1}); 

// Get users in one query
const users = db.find({
  id: {$in: ids} 
});

Analyze and explain plans. Create indexes strategically. Avoid multiple piecemeal queries. Optimize datastore interactions.

14. Improper Error Handling in Promises

Promises simplify asynchronous code. But unhandled rejections are silent failures!

Bad:

function getUser() {
  return fetch('/user')
    .then(r => r.json()); 
}

getUser();

If fetch rejects, exception goes unnoticed. Instead:

Good:

function getUser() {
  return fetch('/user')
    .then(r => r.json())
    .catch(err => console.error(err));
} 

getUser();

Chaining .catch() handles errors properly. Other tips:

  • Avoid promise nesting hell
  • Handle rejections at the top level
  • Configure unhandled rejection tracking

Don't ignore promise errors!

15. Synchronous Network Operations

Network requests should be asynchronous. But sometimes sync variants get used:

Bad:

const data = http.getSync('http://example.com/data'); // blocks!

This stalls the event loop during the request. Instead, use callbacks:

Good:

http.get('http://example.com/data', res => {
  // ...
});

Or promises:

fetch('http://example.com/data')
  .then(res => res.json())
  .then(data => {
    // ...
  });

Async network requests allow other processing while waiting for responses. Avoid synchronous network calls.

16. Inefficient File I/O Operations

Reading/writing files synchronously blocks. For example:

Bad:

const contents = fs.readFileSync('file.txt'); // blocks!

This stalls execution during disk I/O. Instead:

Good:

fs.readFile('file.txt', (err, contents) => {
  // ...
});

// or promises

fs.promises.readFile('file.txt')
   .then(contents => {
     // ...  
   });

This allows the event loop to continue during the file read.

For multiple files, use streams:

function processFiles(files) {
  for (let file of files) {
    fs.createReadStream(file)
      .pipe(/*...*/);
  }
} 

Avoid synchronous file operations. Use callbacks, promises, and streams.

17. Ignoring Performance Profiling and Optimization

It's easy to overlook performance until there are obvious issues. But optimization should be ongoing! Measure first with profiling tools:

  • Browser devtools timeline
  • Node.js profiler
  • third-party profilers

This reveals optimization opportunities even if performance seems fine:

// profile.js
function processOrders(orders) {
  orders.forEach(o => {
    // ...
  });
}

processOrders(allOrders);

The profiler shows processOrders taking 200ms. We investigate and find:

  • Unoptimized loop
  • Expensive inner operation
  • Unnecessary work

We optimize iteratively. The final version takes 5ms!

Profiling guides optimization. Establish performance budgets and fail if exceeded. Measure often and optimize judiciously.

18. Not Utilizing Caching Mechanisms

Caching improves speed by avoiding duplicate work. But it's often forgotten.

Bad:

// Compute expensive report
function generateReport() {
  // ...heavy processing...
}

generateReport(); // Computes
generateReport(); // Computes again!

The same inputs always produce the same output. We should cache:

Good:

// Cache report contents
const cache = {};

function generateReport() {
  if (cache.report) {
    return cache.report;
  }

  const report = // ...compute...

  cache.report = report;
  return report;
}

Now, repeated calls are fast. Other caching strategies:

  • Memory caches like Redis
  • HTTP caching headers
  • LocalStorage for client caching
  • CDNs for asset caching

Identify cache opportunities - they often give big wins!

19. Unnecessary Code Duplication

Duplicated code harms maintainability and optimizability. Consider:

function userStats(user) {
  const name = user.name;
  const email = user.email;
  
  // ...logic...
}

function orderStats(order) {
  const name = order.customerName;
  const email = order.customerEmail;

  // ...logic... 
}

The extraction is duplicated. We refactor:

function getCustomerInfo(data) {
  return {
    name: data.name, 
    email: data.email
  };
}

function userStats(user) {
  const { name, email } = getCustomerInfo(user);
  
  // ...logic...
}

function orderStats(order) {
  const { name, email } = getCustomerInfo(order);

  // ...logic...
}

Now, it's just defined once. Other fixes:

  • Extract utility functions
  • Build helper classes
  • Leverage modules for reusability

Deduplicate whenever you can. It improves code health and optimization.

Conclusion

Optimizing JavaScript application performance is an iterative process. By learning efficient practices and being diligent about profiling, dramatic speed improvements can be achieved.

Key areas to focus on include minimizing DOM changes, leveraging asynchronous techniques, eliminating blocking operations, reducing dependencies, utilizing caching, and removing unneeded duplication.

With attention and experience, you can discover bottlenecks and hone in on optimizations for your specific workload. The result is faster, leaner, and more responsive web apps your users will love.

So be relentless about optimizations - employ these tips and see your JavaScript fly!


If you like my article, feel free to follow me onHackerNoon.


Also published here.


Written by jsdevjournal | 🚀 With a rich decade of software artistry, I am the JavaScript maestro venturing into web, SaaS, AI, and beyond.
Published by HackerNoon on Invalid Date