Modern JavaScript development requires more than just writing code that works. These practical tips will help you write cleaner, faster, and more maintainable JavaScript applications.
1. Use Destructuring for Cleaner Code
Destructuring makes your code more readable and reduces repetition when working with objects and arrays.
// Instead of this
const firstName = user.firstName;
const lastName = user.lastName;
const email = user.email;
// Do this
const { firstName, lastName, email } = user;
// With default values
const { role = 'user', active = true } = user;
// Renaming variables
const { name: userName, id: userId } = user;
// Works with arrays too
const [first, second, ...rest] = numbers;
Why it matters: Reduces boilerplate, improves readability, and makes it clear which properties you're using.
2. Leverage Optional Chaining
Optional chaining prevents runtime errors when accessing nested properties that might not exist.
// Instead of this
const city = user && user.address && user.address.city;
// Do this
const city = user?.address?.city;
// Works with methods and arrays
const result = obj.method?.();
const item = arr?.[0];
Performance benefit: Eliminates the need for multiple null checks and reduces code execution paths.
3. Use Nullish Coalescing for Default Values
The nullish coalescing operator (??) only falls back to the default value when the left side is null or undefined, unlike || which treats falsy values like 0 or empty strings as triggers.
// Problem with ||
const count = 0;
const result = count || 10; // returns 10, not 0!
// Solution with ??
const result = count ?? 10; // returns 0 as expected
// Real-world example
const port = process.env.PORT ?? 3000;
const timeout = options.timeout ?? 5000;
Why it matters: Prevents bugs when dealing with legitimate falsy values like 0, false, or empty strings.
4. Prefer Array Methods Over Loops
Array methods like map, filter, and reduce are more expressive and often easier to optimize by JavaScript engines.
// Instead of traditional loops
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
// Use map
const doubled = numbers.map(n => n * 2);
// Chaining for complex operations
const result = users
.filter(user => user.active)
.map(user => user.name)
.sort();
Maintainability win: Intent is clearer, less room for off-by-one errors, and easier to test.
5. Debounce Expensive Operations
Debouncing limits how often a function executes, crucial for performance with events like scrolling or typing.
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Usage
const handleSearch = debounce((query) => {
fetch(`/api/search?q=${query}`).then(/* handle response */);
}, 300);
input.addEventListener('input', (e) => handleSearch(e.target.value));
Performance impact: Can reduce API calls from hundreds to just a few during rapid user input.
6. Use Object Shorthand and Spread Operator
Modern JavaScript syntax reduces boilerplate and makes object manipulation cleaner.
const name = 'John';
const age = 30;
// Instead of this
const user = { name: name, age: age };
// Do this
const user = { name, age };
// Spread for copying and merging
const updatedUser = { ...user, active: true };
const combined = { ...defaults, ...userSettings };
Why it matters: Less typing, fewer bugs from typos, and clearer intent when merging objects.
7. Avoid Memory Leaks with Proper Cleanup
Always clean up event listeners, timers, and subscriptions to prevent memory leaks.
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
}
mount() {
document.addEventListener('click', this.handleClick);
this.interval = setInterval(() => this.update(), 1000);
}
unmount() {
// Always clean up!
document.removeEventListener('click', this.handleClick);
clearInterval(this.interval);
}
handleClick() { /* ... */ }
update() { /* ... */ }
}
Performance impact: Prevents memory bloat in long-running applications, especially single-page apps.
8. Use Template Literals for String Building
Template literals are more readable and performant than string concatenation.
// Instead of this
const message = 'Hello ' + name + ', you have ' + count + ' messages.';
// Do this
const message = `Hello ${name}, you have ${count} messages.`;
// Multiline strings
const html = `
<div class="user">
<h2>${user.name}</h2>
<p>${user.bio}</p>
</div>
`;
Maintainability: Easier to read, write, and modify, especially for complex strings.
9. Leverage async/await for Readability
Async/await makes asynchronous code look synchronous and is easier to reason about than promise chains.
// Instead of promise chains
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.error(error));
// Use async/await
async function getUserPosts() {
try {
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return posts;
} catch (error) {
console.error('Error:', error);
}
}
Maintainability win: Easier to debug, clearer error handling, and more familiar control flow.
10. Use Object.freeze for Immutability
When you need to ensure an object can't be modified, use Object.freeze() instead of relying on convention.
const config = Object.freeze({
API_URL: 'https://api.example.com',
TIMEOUT: 5000,
MAX_RETRIES: 3
});
// This will fail silently in non-strict mode
config.API_URL = 'https://malicious.com'; // No effect
// Deep freeze for nested objects
function deepFreeze(obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return Object.freeze(obj);
}
Why it matters: Prevents accidental mutations and makes your intent clear to other developers.
11. Memoize Expensive Calculations
Cache the results of expensive function calls to avoid redundant computation.
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Usage
const expensiveCalculation = memoize((n) => {
console.log('Computing...');
return n * n * n;
});
expensiveCalculation(5); // Computing... 125
expensiveCalculation(5); // 125 (from cache, no log)
Performance impact: Can dramatically speed up applications with repeated calculations using the same inputs.
12. Use Early Returns to Reduce Nesting
Early returns make code more readable by eliminating unnecessary nesting.
// Instead of deep nesting
function processUser(user) {
if (user) {
if (user.active) {
if (user.email) {
return sendEmail(user.email);
}
}
}
return null;
}
// Use early returns
function processUser(user) {
if (!user) return null;
if (!user.active) return null;
if (!user.email) return null;
return sendEmail(user.email);
}
Maintainability: Easier to follow the logic and understand exit conditions at a glance.
13. Use Set for Unique Values
Sets provide better performance for uniqueness checks and duplicate removal than arrays.
// Remove duplicates
const numbers = [1, 2, 2, 3, 4, 4, 5];
const unique = [...new Set(numbers)]; // [1, 2, 3, 4, 5]
// Fast lookups
const allowedIds = new Set([1, 2, 3, 4, 5]);
if (allowedIds.has(userId)) {
// O(1) lookup instead of O(n) with array.includes()
}
// Set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1, 2, 3, 4}
const intersection = new Set([...a].filter(x => b.has(x))); // {2, 3}
Performance benefit: Set lookups are O(1) compared to array's O(n), making them much faster for large collections.
14. Use Promise.all for Concurrent Operations
When you have multiple independent async operations, run them concurrently instead of sequentially.
// Sequential (slow)
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
// Total time: sum of all three
// Concurrent (fast)
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
// Total time: time of slowest operation
// Use Promise.allSettled when you want all results even if some fail
const results = await Promise.allSettled([
fetchUser(),
fetchPosts(),
fetchComments()
]);
Performance impact: Can reduce total wait time from seconds to milliseconds when operations don't depend on each other.
15. Use Modern Object Methods
JavaScript provides efficient built-in methods for common object operations.
// Get keys, values, or entries
const user = { name: 'John', age: 30, city: 'NYC' };
Object.keys(user); // ['name', 'age', 'city']
Object.values(user); // ['John', 30, 'NYC']
Object.entries(user); // [['name', 'John'], ['age', 30], ...]
// Convert entries back to object
const entries = [['a', 1], ['b', 2]];
const obj = Object.fromEntries(entries); // { a: 1, b: 2 }
// Clone and merge
const clone = Object.assign({}, original);
const merged = Object.assign({}, defaults, options);
Why it matters: These methods are optimized by engines and more reliable than manual iteration.
16. Use WeakMap for Private Data
WeakMaps allow garbage collection of keys and are perfect for storing private data associated with objects.
const privateData = new WeakMap();
class User {
constructor(name, ssn) {
this.name = name;
privateData.set(this, { ssn }); // SSN stored privately
}
getSSN() {
return privateData.get(this).ssn;
}
}
const user = new User('John', '123-45-6789');
console.log(user.ssn); // undefined (private!)
console.log(user.getSSN()); // '123-45-6789'
Performance benefit: WeakMaps don't prevent garbage collection of their keys, avoiding memory leaks.
17. Use Array.from for Array-like Objects
Convert array-like objects to real arrays for access to array methods.
// Convert NodeList to array
const divs = Array.from(document.querySelectorAll('div'));
divs.forEach(div => console.log(div));
// Create arrays with mapping
const squares = Array.from({ length: 5 }, (_, i) => i * i);
// [0, 1, 4, 9, 16]
// Convert string to array of characters
const chars = Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']
Why it matters: Provides a clean, readable way to work with array-like structures and generate sequences.
18. Use Array.flat for Nested Arrays
Flatten nested arrays without complex recursion.
// Flatten one level
const nested = [1, [2, 3], [4, [5, 6]]];
const flat = nested.flat(); // [1, 2, 3, 4, [5, 6]]
// Flatten completely
const deepFlat = nested.flat(Infinity); // [1, 2, 3, 4, 5, 6]
// flatMap for map + flatten
const sentences = ['Hello world', 'How are you'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'How', 'are', 'you']
Performance benefit: Native implementation is faster than custom recursion.
19. Use Logical Assignment Operators
Combine logical operations with assignment for more concise code.
// OR assignment (||=)
user.role ||= 'guest'; // Assigns 'guest' if role is falsy
// AND assignment (&&=)
user.settings &&= user.settings.theme; // Assigns if settings exists
// Nullish assignment (??=)
options.timeout ??= 5000; // Only assigns if null/undefined
// Practical example
function loadConfig(config = {}) {
config.timeout ??= 3000;
config.retries ??= 3;
config.cache ||= true;
return config;
}
Why it matters: More concise than traditional if statements and makes intent clearer.
20. Use Array.at for Negative Indexing
Access array elements from the end without calculating indices.
const items = ['a', 'b', 'c', 'd'];
// Old way
const last = items[items.length - 1]; // 'd'
const secondLast = items[items.length - 2]; // 'c'
// New way
const last = items.at(-1); // 'd'
const secondLast = items.at(-2); // 'c'
const first = items.at(0); // 'a'
Maintainability: More readable and less error-prone than manual index calculations.
21. Use Throttle for Rate Limiting
Throttling ensures a function executes at most once per time period, ideal for scroll or resize events.
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200);
window.addEventListener('scroll', handleScroll);
Performance impact: Prevents excessive function calls during high-frequency events.
22. Use Object Grouping with Array.reduce
Group array items by a property efficiently.
const users = [
{ name: 'John', role: 'admin' },
{ name: 'Jane', role: 'user' },
{ name: 'Bob', role: 'admin' }
];
const byRole = users.reduce((acc, user) => {
const { role } = user;
acc[role] = acc[role] || [];
acc[role].push(user);
return acc;
}, {});
// { admin: [...], user: [...] }
// Or use the newer Object.groupBy (if available)
const grouped = Object.groupBy(users, user => user.role);
Why it matters: Common pattern for data transformation in dashboards and reports.
23. Use Intersection Observer for Lazy Loading
Efficiently detect when elements enter the viewport without scroll event handlers.
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Load image
observer.unobserve(img); // Stop observing
}
});
}, {
rootMargin: '50px' // Start loading 50px before visible
});
// Observe all images
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
Performance impact: Dramatically improves page load times and reduces bandwidth usage.
24. Use Proxy for Validation and Side Effects
Proxies let you intercept and customize object operations.
const validator = {
set(target, property, value) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (property === 'age' && value < 0) {
throw new RangeError('Age must be positive');
}
target[property] = value;
return true;
}
};
const user = new Proxy({}, validator);
user.age = 30; // OK
// user.age = 'thirty'; // Throws TypeError
// user.age = -5; // Throws RangeError
Why it matters: Provides runtime validation and can track property access for debugging.
25. Use AbortController for Cancellable Requests
Cancel fetch requests when they're no longer needed.
const controller = new AbortController();
const signal = controller.signal;
// Start a fetch
fetch('/api/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request cancelled');
}
});
// Cancel the request
controller.abort();
// Practical example: cancel on component unmount
class SearchComponent {
search(query) {
this.controller?.abort(); // Cancel previous request
this.controller = new AbortController();
return fetch(`/api/search?q=${query}`, {
signal: this.controller.signal
});
}
cleanup() {
this.controller?.abort();
}
}
Performance impact: Prevents wasted bandwidth and processing of outdated requests.
26. Use Performance.now for Accurate Timing
Measure code execution time with high precision.
// Instead of Date.now()
const start = Date.now();
// ... code ...
const end = Date.now();
console.log(`Took ${end - start}ms`); // Low precision
// Use performance.now()
const start = performance.now();
// ... code ...
const end = performance.now();
console.log(`Took ${(end - start).toFixed(2)}ms`); // High precision
// Measure function performance
function measurePerf(fn, ...args) {
const start = performance.now();
const result = fn(...args);
const duration = performance.now() - start;
console.log(`${fn.name} took ${duration.toFixed(2)}ms`);
return result;
}
Why it matters: More accurate measurements for performance optimization.
27. Use Object Destructuring in Function Parameters
Make function signatures clearer and provide default values easily.
// Instead of this
function createUser(name, age, email, role, active) {
// Easy to mix up parameter order
}
// Do this
function createUser({ name, age, email, role = 'user', active = true }) {
// Clear what each parameter is, with defaults
return { name, age, email, role, active };
}
// Call with named parameters
const user = createUser({
name: 'John',
email: 'john@example.com',
age: 30
});
Maintainability: Parameter order doesn't matter, self-documenting, and easy to add new parameters.
28. Use Map for Object-like Structures with Better Performance
Maps provide better performance and more features than plain objects for key-value storage.
// Maps allow any type as key
const cache = new Map();
const objKey = { id: 1 };
cache.set(objKey, 'some value');
cache.get(objKey); // 'some value'
// Easy iteration
cache.forEach((value, key) => {
console.log(key, value);
});
// Check existence
if (cache.has(objKey)) {
// ...
}
// Get size
console.log(cache.size); // No need for Object.keys().length
// Clear all entries
cache.clear();
Performance benefit: Better performance for frequent additions/deletions, maintains insertion order, and allows non-string keys.
29. Use String Methods for Cleaner String Operations
Modern string methods make common operations more readable.
// Check string contents
const text = 'Hello World';
text.startsWith('Hello'); // true
text.endsWith('World'); // true
text.includes('lo Wo'); // true
// Padding
'5'.padStart(3, '0'); // '005'
'5'.padEnd(3, '0'); // '500'
// Repeat
'*'.repeat(10); // '**********'
// Replace all occurrences
const str = 'foo foo foo';
str.replaceAll('foo', 'bar'); // 'bar bar bar'
// Trim variants
' hello '.trim(); // 'hello'
' hello '.trimStart(); // 'hello '
' hello '.trimEnd(); // ' hello'
Why it matters: More expressive than regex for common operations and easier to understand.
30. Use requestAnimationFrame for Smooth Animations
Schedule animations to run at optimal times for smooth performance.
// Instead of setInterval
let position = 0;
setInterval(() => {
position += 1;
element.style.left = position + 'px';
}, 16); // Trying to hit 60fps manually
// Use requestAnimationFrame
let position = 0;
function animate() {
position += 1;
element.style.left = position + 'px';
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
// With timestamp for consistent speed
function animate(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
element.style.left = Math.min(progress / 10, 500) + 'px';
if (progress < 5000) {
requestAnimationFrame(animate);
}
}
Performance impact: Animations run at optimal frame rates and pause when tab is inactive.
31. Use structuredClone for Deep Copying
Create true deep copies without external libraries.
// Instead of JSON.parse(JSON.stringify(obj))
const original = {
name: 'John',
date: new Date(),
nested: { value: 42 },
fn: () => console.log('test') // Lost with JSON method
};
// Deep clone with structuredClone
const clone = structuredClone(original);
clone.nested.value = 99;
console.log(original.nested.value); // Still 42
// Note: Functions are not cloned
// But dates, maps, sets, etc. are preserved correctly
Why it matters: Handles more types than JSON methods and doesn't lose data like Dates, Maps, and Sets.
32. Use BigInt for Large Numbers
Handle integers larger than Number.MAX_SAFE_INTEGER accurately.
// Regular numbers lose precision
const big = 9007199254740992;
console.log(big + 1); // 9007199254740992 (wrong!)
// Use BigInt
const bigInt = 9007199254740992n;
console.log(bigInt + 1n); // 9007199254740993n (correct!)
// Convert from string for very large numbers
const huge = BigInt('999999999999999999999999999999');
// Note: Can't mix BigInt with regular numbers
// bigInt + 1 // TypeError
// bigInt + 1n // OK
Why it matters: Essential for financial calculations, cryptography, and working with large IDs.
33. Use Promise.race for Timeout Patterns
Implement timeouts and race conditions elegantly.
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
}
// Race between fetch and timeout
try {
const data = await Promise.race([
fetch('/api/data').then(r => r.json()),
timeout(5000)
]);
console.log(data);
} catch (error) {
console.error('Request timed out or failed');
}
// First successful response
const fastestResponse = await Promise.race([
fetch('/api/server1/data'),
fetch('/api/server2/data'),
fetch('/api/server3/data')
]);
Performance impact: Prevents slow requests from blocking your application.
34. Use Object.hasOwn Instead of hasOwnProperty
Safer way to check if an object has its own property.
const obj = { name: 'John' };
// Old way (can fail if object has no prototype)
obj.hasOwnProperty('name'); // true
// New way (safer)
Object.hasOwn(obj, 'name'); // true
// Why it's better
const nullObj = Object.create(null);
// nullObj.hasOwnProperty('name'); // TypeError
Object.hasOwn(nullObj, 'name'); // false (works!)
Why it matters: More reliable and works with all objects, including those without prototypes.
35. Use Array.findLast for Reverse Searches
Find elements from the end of an array efficiently.
const numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
// Old way
const lastEven = [...numbers].reverse().find(n => n % 2 === 0);
// New way (more efficient)
const lastEven = numbers.findLast(n => n % 2 === 0); // 4
// Also findLastIndex
const lastIndex = numbers.findLastIndex(n => n % 2 === 0); // 5
Performance benefit: No need to reverse the array or iterate through the entire array unnecessarily.
Conclusion
These 35 JavaScript tips cover a wide range of patterns and techniques that will make your code more performant, maintainable, and modern. By adopting these practices, you'll write code that's easier to understand, debug, and scale. Start by incorporating a few tips that address your current pain points, then gradually adopt more as they become relevant to your projects. Remember, the goal isn't to use every technique in every project, but to have these tools in your toolkit when you need them.
Top comments (0)