Modern JavaScript Use: Tips and Tricks for Developers

What is modern JavaScript? Modern JavaScript is the version of the language shaped by ECMAScript 2015 (ES6) and the features added in the yearly editions since, which together replaced older patterns with cleaner, safer, more modular ways to write code.

Tarun Sharma
Tarun Sharma Founder, Chetaru
|
Updated Jun 23, 2026
|
11 min read
Share

Need More Growth & Leads?

We are ready to work with your business and generate some real results…

Let's Talk

What is modern JavaScript?

Modern JavaScript is the version of the language shaped by ECMAScript 2015 (ES6) and the features added in the yearly editions since, which together replaced older patterns with cleaner, safer, more modular ways to write code. ES2015 landed in June 2015 and was the largest update in the language’s history, and TC39, the committee that governs the language, has shipped a new edition every year since (MDN). JavaScript is also the most widely used programming language, reported by 62% of developers in Stack Overflow’s 2024 survey (Stack Overflow), so writing it well matters across almost every web project.

Key Takeaways

  • Modern JavaScript starts with ES2015 (June 2015) and grows through a new edition every year, governed by TC39 (MDN).
  • Core features to adopt are let/const, arrow functions, template literals, destructuring, classes, and ES modules (MDN).
  • async and defer stop scripts from blocking page rendering (MDN).
  • Code splitting, dynamic import() (standardised in ES2020), and tree shaking cut the JavaScript a browser has to download and run (MDN).
  • JavaScript is the most-used language among developers, at 62% in 2024 (Stack Overflow).

JavaScript began as a small scripting language for making web pages interactive and has become the foundation of complex applications that run in the browser, on the server through Node.js, in desktop apps, and even on edge networks. That reach is part of why keeping current with the language pays off across so many different kinds of project. This guide covers the features worth adopting and the loading techniques that keep modern JavaScript fast: the ES6+ syntax you’ll use daily, then async/defer, code splitting, dynamic imports, and tree shaking. It builds naturally on our guide to reducing unused JavaScript.

Which ES6+ features define modern JavaScript?

The features that define modern JavaScript are block-scoped variables, arrow functions, template literals, destructuring, classes, and modules, all introduced in ES2015 and supported in every current browser (MDN). They aren’t just shorter to type; each one removes a class of bug that older JavaScript was prone to. The table below summarises what each adds.

Feature What it does Replaces
let / const Block-scoped variables var and its function-scope surprises
Arrow functions Short syntax, lexical this function expressions with this bugs
Template literals String interpolation with backticks String concatenation with +
Destructuring Pull values out of objects and arrays Repeated property access
Classes Clear syntax for object blueprints Prototype boilerplate
Modules import/export between files Global variables and script ordering

How do let and const improve your code?

let and const give you block scoping, so a variable exists only inside the { } block where it’s declared, which is far more predictable than the function scope of the old var (MDN). Use const by default for values that never get reassigned, and let only when you genuinely need to reassign. Reserving var for legacy code is the safest habit.

let count = 25;
count = 26; // fine: let allows reassignment
const name = 'Alice';
name = 'Bob'; // TypeError: assignment to constant variable

Note that const prevents reassignment of the binding, not mutation of the value. A const array can still have items pushed to it; you just can’t point the name at a new array.

What are arrow functions and when should you use them?

Arrow functions give you a shorter function syntax and, more importantly, they inherit this from the surrounding scope instead of defining their own (MDN). That single change removes one of the most common sources of confusion in older code, where this inside a callback pointed somewhere unexpected.

// Traditional function
function add(a, b) { return a + b; }
// Arrow function
const add = (a, b) => a + b;

Reach for arrow functions in callbacks and short expressions. Keep regular functions where you need a method that uses its own this, or a function that needs to be hoisted.

How do template literals and destructuring help?

Template literals let you build strings with backticks and ${ } interpolation, which is clearer than gluing strings together with +, and they support multi-line strings directly (MDN). Destructuring lets you pull values out of objects and arrays into named variables in one line, cutting repetitive property access.

const user = { name: 'Alice', age: 25 };
const { name, age } = user; // destructuring
console.log(`${name} is ${age}`); // template literal -> "Alice is 25"

Together they make data handling read like the intent behind it, which matters most when you return to code months later.

How do classes and modules organise larger code?

Classes give you a clear syntax for creating objects with shared properties and methods, replacing the prototype boilerplate that older JavaScript required (MDN). Modules let you split code across files and share specific pieces with export and import, instead of leaking everything onto the global scope and depending on script order.

// person.js
export class Person {
  constructor(name) { this.name = name; }
  greet() { return `Hello, I'm ${this.name}`; }
}
// app.js
import { Person } from './person.js';
const alice = new Person('Alice');

ES modules also unlock the build-time optimisations covered later, because a bundler can see exactly which exports each file actually uses. The spread (...) and rest operators round out the everyday toolkit, letting you copy and merge arrays and objects or gather arguments into one parameter.

How do promises and async/await handle asynchronous code?

Promises and async/await are how modern JavaScript handles operations that take time, such as fetching data, without the tangled callbacks older code relied on. Promises arrived in ES2015 and represent a value that will be available later, while async/await, added in ES2017, lets you write asynchronous code that reads top to bottom like ordinary synchronous code (MDN). The combination is one of the largest readability gains in the language.

A function marked async always returns a promise, and await pauses inside it until a promise settles, so you handle the result on the next line instead of nesting callbacks:

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    const user = await res.json();
    return user;
  } catch (err) {
    console.error('Failed to load user', err);
  }
}

Compared with chained .then() calls or nested callbacks, this is easier to read and debug, and errors flow through a normal try/catch. Use async/await for sequential steps that depend on each other, and reach for Promise.all() when you have independent operations that can run at once, which avoids awaiting them one after another when they don’t need to be.

How do async and defer speed up script loading?

async and defer are attributes on the <script> tag that stop a script from blocking the browser as it builds the page, which is one of the most common causes of slow first loads (MDN). By default, a plain <script> tag is render-blocking: the browser pauses parsing the HTML, downloads the script, runs it, and only then continues. On a page with several scripts, that stalls everything the user is waiting to see.

async downloads the script in parallel and runs it as soon as it’s ready, which can interrupt parsing and runs scripts out of order. defer also downloads in parallel but waits until the HTML is fully parsed, then runs deferred scripts in the order they appear. The difference matters:

Attribute Download Execution Best for
none Blocks parsing Immediately, blocks rendering Critical inline logic only
async Parallel As soon as ready, any order Independent scripts (analytics)
defer Parallel After parse, in order App scripts that depend on the DOM or each other
<script src="analytics.js" async></script>
<script src="app.js" defer></script>

As a rule, use defer for your application code, because it preserves order and guarantees the DOM is ready, and use async for self-contained third-party scripts that don’t depend on anything else. Eliminating render-blocking scripts is a core performance fix; our guide to eliminating render-blocking resources goes further.

What is code splitting and why does it matter?

Code splitting is the practice of breaking your JavaScript into smaller bundles that load on demand, instead of shipping one large file the browser must download and parse before anything runs (webpack). It matters because JavaScript is expensive: the browser has to download it, parse it, and execute it on the main thread, and large bundles directly delay when a page becomes interactive.

The idea is simple. Rather than one bundle.js containing your entire app, you split it so the browser loads only the code a given page or feature needs, then fetches the rest when the user navigates or triggers it. A login screen doesn’t need the dashboard’s charts library on first paint. Modern bundlers handle the mechanics:

  • webpack splits by entry points, shared chunks, and dynamic imports.
  • Vite and Rollup split automatically around dynamic imports during the production build.
  • esbuild and Parcel offer splitting with minimal configuration.

The payoff is a smaller initial download and faster time to interactive, which is exactly the kind of waste our guide to reducing unused JavaScript targets.

How does dynamic import work?

Dynamic import loads a module at runtime by calling import() as a function, which returns a promise that resolves to the module (MDN). Standardised in ES2020, it’s the mechanism that makes code splitting practical: the bundler sees the import() call and automatically creates a separate chunk that only downloads when the code actually runs.

button.addEventListener('click', async () => {
  const { renderChart } = await import('./chart.js');
  renderChart(data);
});

Here the charting code only downloads when the user clicks the button, not on page load. Use dynamic import for features behind user interaction, routes in a single-page app, and heavy libraries that most visitors never touch. The trade-off is a small network request at the moment of use, so reserve it for code that’s genuinely optional rather than splitting everything into tiny fragments.

What is tree shaking and how do you enable it?

Tree shaking is the build step that removes code you import but never actually use, named for the idea of shaking a tree so the dead leaves fall off (webpack). It relies on ES modules, because the static import/export syntax lets a bundler analyse exactly which exports are referenced and drop the rest before the code ever reaches the browser.

Enabling it is mostly a matter of configuration. In webpack, you build in production mode, use ES module syntax throughout, and mark your package side-effect-free so the bundler knows it’s safe to drop unused exports:

// package.json
{
  "sideEffects": false
}

Vite, Rollup, and esbuild tree-shake by default in their production builds, so for most modern setups the main job is to import only what you need (import { debounce } from 'lodash-es', not the whole library) and to avoid code with hidden side effects that the bundler can’t safely remove. The result is a smaller bundle without changing what your app does.

How do you put modern JavaScript into practice?

Put modern JavaScript into practice by writing ES module code, running it through a bundler, and targeting current browsers, which lets you ship clean syntax without the heavy transpilation older projects needed (MDN). Browser support for ES2015+ is effectively universal now, so most of the workarounds from a few years ago are no longer worth their cost. A few habits keep the benefits compounding:

  • Default to const, then let. Reassignment becomes a deliberate choice, not an accident.
  • Use a linter such as ESLint to catch unsafe patterns and enforce a consistent style across a team.
  • Bundle and split. Let your build tool produce small, on-demand chunks rather than one monolith.
  • Import precisely. Pull in the named exports you use so tree shaking can drop the rest.
  • Measure the result. Check your JavaScript’s effect on load time with Lighthouse, as covered in our guide to improving your Lighthouse score, and confirm scripts aren’t blocking scroll, which we cover in improving scroll performance.

The throughline is that modern JavaScript is as much about what you don’t ship as what you write. Clean syntax makes code readable; loading techniques make it fast.

Frequently asked questions

ES6, formally ECMAScript 2015, is the release that started modern JavaScript, but the term now covers everything added since through the yearly editions (MDN). Features like dynamic import() (ES2020), optional chaining, and async/await came after ES6. So “ES6” and “modern JavaScript” overlap heavily, but modern JavaScript is the broader, still-growing set.

Final thoughts

Modern JavaScript rewards two kinds of discipline. The syntax, const, arrow functions, destructuring, classes, and modules, makes your code clearer and removes whole categories of bug. The loading techniques, async/defer, code splitting, dynamic imports, and tree shaking, make sure all that code reaches users quickly instead of stalling the page. Start by adopting ES modules and a bundler, default to const, and split off the heavy parts of your app that not every visitor needs. Then measure with Lighthouse and adjust, because the goal isn’t using every feature, it’s shipping less code that does more.