How To Write Fast Memory-Efficient JavaScript

Sandeep Agrawal
6 min readMay 25, 2024

--

In today’s fast-paced digital world, performance is crucial. Whether building a complex web application or a simple website, fast Memory-Efficient JavaScript can significantly enhance the user experience.

This blog post will guide you through best practices and techniques for writing fast, memory-efficient JavaScript.

Overview in Tabular Format

Detailed Descriptions

1. Understand JavaScript Performance Bottlenecks

  • Common bottlenecks include:
  • DOM Manipulation: Frequent and complex DOM updates can slow down your application.
  • Memory Leaks: Unreleased memory can cause your application to consume more memory over time.
  • Inefficient Algorithms: Poorly designed algorithms can drastically reduce performance.

2. Minimize DOM Manipulations

  • Group multiple DOM updates together to reduce reflows and repaints.
  • Use a virtual DOM whenever possible to batch changes before applying them to the actual DOM.
  • Use a DOM DocumentFragment.
  • Bad Code Example:
const list = document.getElementById('list');

for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item);
}
  • Good Code Example:
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}

document.getElementById('list').appendChild(fragment);

3. Memory Management

  • Use const and let Instead of var: const and let have block scope, which helps in better memory management compared to var.
  • Avoid Global Variables: Global variables remain in memory throughout the lifecycle of your application. Use local variables whenever possible.
  • Clean Up Event Listeners: Remove event listeners when they are no longer needed to prevent memory leaks.
  • Bad Code Example:
var counter = 0;

function increment() {
counter++;
}

document.getElementById('btn').addEventListener('click', increment);
  • Good Code Example:
let counter = 0;
const increment = () => {
counter++;
};
const button = document.getElementById('btn');
button.addEventListener('click', increment);

// Later in your code
button.removeEventListener('click', increment);

4. Optimize Loops and Iterations

  • Avoid deep nesting and cache array lengths to improve loop performance.
  • Bad Code Example:
const items = [/* large array */];

for (let i = 0; i < items.length; i++) {
// process items[i]
}
  • Good Code Example:
const items = [/* large array */];

for (let i = 0, len = items.length; i < len; i++) {
// process items[i]
}

5. Optimize Object and Array Creation

  • Pre-allocate objects and arrays to their expected size to reduce the overhead of resizing.
  • Improve performance in scenarios where large objects or arrays are frequently created.
  • Bad Code Example:
const arr = [];
for (let i = 0; i < 1000; i++) {
arr.push(i); // Array resizing can be inefficient
}
  • Good Code Example:
const arr = new Array(1000);
for (let i = 0; i < 1000; i++) {
arr[i] = i; // Pre-allocated, more efficient
}

6. Optimize Functions and Closures

  • Avoid creating functions inside loops.
  • Use debouncing or throttling for frequently firing events (e.g., scroll, resize).
  • Bad Code Example:
const elements = document.querySelectorAll('.element');

elements.forEach((el, i) => {
el.addEventListener('click', function() {
console.log(i);
});
});
  • Good Code Example:
function handleClick(i) {
console.log(i);
}

const elements = document.querySelectorAll('.element');

elements.forEach((el, i) => {
el.addEventListener('click', handleClick.bind(null, i));
});

// Debouncing Example
function debounce(func, wait) {
let timeout;

return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
window.addEventListener('resize', debounce(function() {
console.log('Resized');
}, 250));

7. Use Arrow Functions Appropriately

  • Use arrow functions for concise syntax and to avoid issues with this binding. However, be cautious of memory usage when creating functions within loops or frequently called functions.
  • It simplifies code and prevents common pitfalls with this binding in methods.
  • Bad Code Example:
function Timer() {
this.seconds = 0;
setInterval(function() {
this.seconds++;
}, 1000); // `this` is not bound correctly
}
  • Good Code Example:
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
}, 1000); // `this` is correctly bound
}

8. Use Efficient Data Structures

Choose the right data structures for your needs:

  • Use Maps and Sets: For operations involving frequent additions and deletions, Map and Set can be more efficient than objects and arrays.
  • Bad Code Example:
const obj = {};
obj['key1'] = 'value1';
console.log(obj['key1']); // 'value1'
delete obj['key1'];
  • Good Code Example:
const map = new Map();
map.set('key1', 'value1');
console.log(map.get('key1')); // 'value1'
map.delete('key1');

9. Defer and Async for Script Loading

  • Use defer and async attributes on <script> tags to load JavaScript files without blocking the rendering of the page.
  • It can improve initial page load times by optimizing script loading.
  • Example:
<script src="script.js" defer></script> <!-- Defer script loading -->
<script src="script.js" async></script> <!-- Async script loading -->

10. Lazy Loading

  • Load resources like images and scripts only when needed to reduce initial load time.
  • Bad Code Example:
<img src="large-image.jpg" alt="Large Image">
  • Good Code Example:
<img src="placeholder.jpg" data-src="large-image.jpg" alt="Large Image" class="lazy-load">

<script>
document.addEventListener('DOMContentLoaded', function() {
const lazyImages = document.querySelectorAll('.lazy-load');
const lazyLoad = function() {
lazyImages.forEach(img => {
if (img.getBoundingClientRect().top < window.innerHeight) {
img.src = img.dataset.src;
img.classList.remove('lazy-load');
}
});
};
window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
});
</script>

11. Avoid Inline Styles and Event Handlers

  • Avoid inline styles and event handlers to keep HTML clean and improve the separation of concerns.
  • Improve maintainability and potential performance by separating HTML, CSS, and JavaScript.
  • Bad Code Example:
<button style="color: red;" onclick="handleClick()">Click me</button>
  • Good Code Example:
<button class="btn-red" id="myButton">Click me</button>
<style>
.btn-red {
color: red;
}
</style>
<script>
document.getElementById('myButton').addEventListener('click', handleClick);
</script>

12. Avoid Inefficient Algorithms

  • Optimize algorithms to reduce computational complexity and improve performance.
  • Bad Code Example:
function findDuplicates(arr) {
const duplicates = [];
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
duplicates.push(arr[i]);
}
}
}
return duplicates;
}
  • Good Code Example:
function findDuplicates(arr) {
const seen = new Set();
const duplicates = new Set();
for (const item of arr) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}
return Array.from(duplicates);
}

13. Leverage Browser Caching

  • Utilize browser caching for static resources to reduce load times for returning visitors.
  • This can improve performance by reducing the need to download static resources repeatedly.
  • Example: Configure the server to set appropriate cache headers for static assets.
Cache-Control: max-age=31536000, public

14. Limit Use of Third-Party Libraries

  • Be selective with third-party libraries to avoid unnecessary bloat and potential security vulnerabilities.
  • Keep your application lightweight and secure by minimizing dependencies whenever possible.
  • Example: Prefer using built-in or lightweight libraries:
// Prefer built-in methods over large libraries for simple tasks
const uniqueItems = Array.from(new Set(array));

15. Profile and Monitor Performance

Use browser developer tools to profile and monitor your application’s performance:

  • Performance Tab: Record and analyze the runtime performance of your application.
  • Memory Tab: Identify memory leaks and monitor memory usage.

16. Use Web Workers

  • Offload heavy computations to Web Workers to prevent blocking the main thread.
  • Improves performance in applications with intensive computations by running scripts in the background.
  • Bad Code Example:
function heavyComputation() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
console.log(sum);
}
heavyComputation(); // Blocks the main thread
  • Good Code Example:
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('Sum:', event.data);
};
worker.postMessage('start');

// worker.js
onmessage = function(event) {
if (event.data === 'start') {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
postMessage(sum);
}
};

17. Use Service Workers for Caching

  • Implement service workers to cache assets and efficiently handle network requests.
  • Example:
// Registering a service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}

Conclusion

Writing fast, memory-efficient JavaScript involves a combination of best practices, efficient coding techniques, and careful monitoring.

By understanding performance bottlenecks, minimizing DOM manipulations, optimizing loops, managing memory effectively, using appropriate data structures, leveraging lazy loading, and avoiding inefficient algorithms, you can significantly enhance the performance of your JavaScript applications.

Always profile and monitor your application to identify bottlenecks and ensure your optimizations are effective.

References:

Originally published at https://techtalkbook.com on May 25, 2024.

--

--