30 Years of JavaScript: The Complete Evolution Guide
December 4, 2025 - A comprehensive guide for developers returning to JavaScript or learning its complete history.
Table of Contents
- The Beginning: JavaScript 1.0 - 1.5 (1995-2000)
- The Dark Ages: Browser Wars & Stagnation (2000-2005)
- The AJAX Revolution (2005-2009)
- The Modern Era Begins: ES5 & Node.js (2009-2015)
- The Renaissance: ES6/ES2015 (2015)
- Annual Releases: ES2016-ES2025
- TypeScript Evolution (2012-2025)
- Server-Side JavaScript: Node.js & Beyond
- Frameworks & Libraries: Rise and Fall
- Testing Ecosystem Evolution
- Build Tools & Module Systems
- Package Management
- Current Landscape (2025)
The Beginning: JavaScript 1.0 - 1.5 (1995-2000)
1995: The Birth
Brendan Eich created JavaScript in 10 days at Netscape in May 1995. Originally named "Mocha," then "LiveScript," finally "JavaScript."
JavaScript 1.0 (December 1995) - Netscape Navigator 2.0
Basic features:
// Variables (no let/const, only var)
var name = "John";
var age = 25;
// Functions
function greet(name) {
return "Hello, " + name;
}
// Objects (prototype-based)
var person = {
name: "John",
age: 25,
greet: function() {
return "Hello, " + this.name;
}
};
// Arrays
var numbers = [1, 2, 3, 4, 5];
// Loops
for (var i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
// Conditionals
if (age >= 18) {
console.log("Adult");
} else {
console.log("Minor");
}
1996-1997: Standardization
ECMAScript 1 (June 1997) - First standardized version
Microsoft created JScript for Internet Explorer 3.0, leading to fragmentation.
1998-1999: ECMAScript 2 & 3
ECMAScript 3 (December 1999) - Major update
Added:
- Regular expressions
- Better string handling
- try/catch exception handling
- switch statements
- do-while loops
// Regular expressions
var pattern = /hello/i;
var text = "Hello World";
console.log(pattern.test(text)); // true
// try/catch
try {
throw new Error("Something went wrong");
} catch (e) {
console.log(e.message);
} finally {
console.log("Cleanup");
}
// switch
var day = "Monday";
switch (day) {
case "Monday":
console.log("Start of week");
break;
case "Friday":
console.log("End of week");
break;
default:
console.log("Midweek");
}
The Dark Ages: Browser Wars & Stagnation (2000-2005)
The Problem
- ECMAScript 4 was abandoned due to disagreements
- Browser incompatibilities were rampant
- JavaScript was considered a "toy language"
- No standard library management
- Security issues (XSS attacks)
Browser Detection Hell
// Typical code from this era
if (document.all) {
// Internet Explorer
document.all.myElement.style.color = "red";
} else if (document.getElementById) {
// Standards-compliant browsers
document.getElementById("myElement").style.color = "red";
}
// Or worse
if (navigator.userAgent.indexOf("MSIE") > -1) {
// IE-specific code
}
Early Libraries (Pre-jQuery)
Prototype.js (2005)
// Prototype.js extended native objects
$('myElement').hide();
// Array extensions
[1, 2, 3, 4, 5].each(function(n) {
console.log(n);
});
The AJAX Revolution (2005-2009)
2005: AJAX Changes Everything
AJAX (Asynchronous JavaScript and XML) - Term coined by Jesse James Garrett
// XMLHttpRequest - The old way
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
2006: jQuery Revolution
jQuery 1.0 by John Resig - Changed everything
Before jQuery:
// Cross-browser event handling nightmare
var element = document.getElementById('myButton');
if (element.addEventListener) {
element.addEventListener('click', handleClick, false);
} else if (element.attachEvent) {
element.attachEvent('onclick', handleClick);
}
// DOM manipulation
var elements = document.getElementsByTagName('p');
for (var i = 0; i < elements.length; i++) {
elements[i].style.color = 'red';
}
// AJAX
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById('result').innerHTML = xhr.responseText;
}
};
xhr.send();
With jQuery:
// Simple, elegant, cross-browser
$('#myButton').click(handleClick);
// DOM manipulation
$('p').css('color', 'red');
// AJAX
$.get('/api/users', function(data) {
$('#result').html(data);
});
// Chaining
$('#myDiv')
.addClass('active')
.fadeIn()
.css('color', 'blue')
.html('Hello World');
// Powerful selectors
$('div.container > p:first-child').hide();
Why jQuery dominated:
- Cross-browser compatibility
- Simple, intuitive API
- Powerful DOM manipulation
- Easy AJAX
- Plugin ecosystem
- "Write less, do more"
2008-2009: Other Libraries
MooTools (2006)
// Object-oriented approach
var MyClass = new Class({
initialize: function(name) {
this.name = name;
},
greet: function() {
alert('Hello ' + this.name);
}
});
Dojo Toolkit (2004-2009)
// Enterprise-focused
dojo.require("dijit.form.Button");
dojo.ready(function() {
new dijit.form.Button({
label: "Click me!",
onClick: function() { alert("Clicked!"); }
}, "myButton");
});
The Modern Era Begins: ES5 & Node.js (2009-2015)
2009: ECMAScript 5 (ES5)
After 10 years of stagnation, ES5 brought crucial improvements.
Strict Mode:
// Without strict mode (sloppy mode)
function oldWay() {
x = 10; // Creates global variable accidentally
var arguments = 5; // Can override arguments
delete Object.prototype; // Can delete built-ins
}
// With strict mode
"use strict";
function newWay() {
x = 10; // ReferenceError: x is not defined
var arguments = 5; // SyntaxError
delete Object.prototype; // TypeError
}
Array Methods:
var numbers = [1, 2, 3, 4, 5];
// forEach - Before: for loop
numbers.forEach(function(num) {
console.log(num);
});
// map - Transform arrays
var doubled = numbers.map(function(num) {
return num * 2;
}); // [2, 4, 6, 8, 10]
// filter - Filter arrays
var evens = numbers.filter(function(num) {
return num % 2 === 0;
}); // [2, 4]
// reduce - Aggregate
var sum = numbers.reduce(function(acc, num) {
return acc + num;
}, 0); // 15
// some & every
var hasEven = numbers.some(function(num) {
return num % 2 === 0;
}); // true
var allPositive = numbers.every(function(num) {
return num > 0;
}); // true
Object Methods:
// Object.create - Proper prototypal inheritance
var parent = {
greet: function() {
return "Hello";
}
};
var child = Object.create(parent);
child.greet(); // "Hello"
// Object.keys
var obj = { a: 1, b: 2, c: 3 };
Object.keys(obj); // ['a', 'b', 'c']
// Object.defineProperty - Property descriptors
var person = {};
Object.defineProperty(person, 'name', {
value: 'John',
writable: false,
enumerable: true,
configurable: false
});
person.name = 'Jane'; // Silently fails (throws in strict mode)
// Getters and setters
var account = {
_balance: 0,
get balance() {
return this._balance;
},
set balance(value) {
if (value < 0) throw new Error("Balance cannot be negative");
this._balance = value;
}
};
account.balance = 100;
console.log(account.balance); // 100
JSON Native Support:
// Before: eval() or libraries
var jsonString = '{"name":"John","age":30}';
var obj = eval('(' + jsonString + ')'); // Dangerous!
// ES5: Native JSON
var obj = JSON.parse('{"name":"John","age":30}');
var str = JSON.stringify({ name: "John", age: 30 });
Function.prototype.bind:
// Before: Manual context binding
var obj = {
name: "John",
greet: function() {
console.log("Hello, " + this.name);
}
};
var greet = obj.greet;
greet(); // "Hello, undefined" - lost context
// Workaround
var self = obj;
setTimeout(function() {
self.greet();
}, 1000);
// ES5: bind
var boundGreet = obj.greet.bind(obj);
boundGreet(); // "Hello, John"
setTimeout(obj.greet.bind(obj), 1000);
2009: Node.js - JavaScript on the Server
Ryan Dahl created Node.js, bringing JavaScript to the server.
Before Node.js:
- JavaScript only in browsers
- Server-side: PHP, Ruby, Python, Java, .NET
With Node.js:
// Simple HTTP server
var http = require('http');
var server = http.createServer(function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(3000);
console.log('Server running at http://localhost:3000/');
Event-driven, non-blocking I/O:
// Blocking (traditional server-side)
var data = readFileSync('file.txt'); // Waits
processData(data);
// Non-blocking (Node.js)
readFile('file.txt', function(err, data) {
if (err) throw err;
processData(data);
});
// Continues immediately
CommonJS Modules:
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add: add,
subtract: subtract
};
// app.js
var math = require('./math');
console.log(math.add(5, 3)); // 8
2010: npm - Node Package Manager
npm revolutionized package management.
# Install packages
npm install express
# package.json
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.17.1"
}
}
Express.js (2010) - Web framework for Node.js
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send('Hello World!');
});
app.get('/users/:id', function(req, res) {
res.json({ id: req.params.id, name: 'John' });
});
app.listen(3000);
2010-2012: The Framework Wars Begin
Backbone.js (2010) - First major MV* framework
// Model
var Todo = Backbone.Model.extend({
defaults: {
title: '',
completed: false
}
});
// Collection
var TodoList = Backbone.Collection.extend({
model: Todo
});
// View
var TodoView = Backbone.View.extend({
tagName: 'li',
template: _.template('<%= title %>'),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
// Usage
var todo = new Todo({ title: 'Learn Backbone' });
var view = new TodoView({ model: todo });
$('#todos').append(view.render().el);
Knockout.js (2010) - MVVM with observables
function AppViewModel() {
this.firstName = ko.observable("John");
this.lastName = ko.observable("Doe");
this.fullName = ko.computed(function() {
return this.firstName() + " " + this.lastName();
}, this);
}
ko.applyBindings(new AppViewModel());
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"></span>!</h2>
AngularJS 1.0 (2010, released 2012) - Google's framework
// Controller
angular.module('myApp', [])
.controller('TodoController', function($scope) {
$scope.todos = [];
$scope.addTodo = function() {
$scope.todos.push({
text: $scope.todoText,
done: false
});
$scope.todoText = '';
};
$scope.remaining = function() {
return $scope.todos.filter(function(todo) {
return !todo.done;
}).length;
};
});
<div ng-app="myApp" ng-controller="TodoController">
<input ng-model="todoText" placeholder="Add new todo">
<button ng-click="addTodo()">Add</button>
<ul>
<li ng-repeat="todo in todos">
<input type="checkbox" ng-model="todo.done">
<span ng-class="{done: todo.done}">{{todo.text}}</span>
</li>
</ul>
<p>{{remaining()}} of {{todos.length}} remaining</p>
</div>
Ember.js (2011) - Convention over configuration
// Router
App.Router.map(function() {
this.route('about');
this.route('posts', function() {
this.route('post', { path: '/:post_id' });
});
});
// Route
App.PostsRoute = Ember.Route.extend({
model: function() {
return this.store.findAll('post');
}
});
// Controller
App.PostsController = Ember.ArrayController.extend({
sortProperties: ['date'],
sortAscending: false
});
// Template (Handlebars)
{{#each post in controller}}
<article>
<h2>{{post.title}}</h2>
<p>{{post.body}}</p>
</article>
{{/each}}
2012: TypeScript Arrives
Microsoft releases TypeScript 0.8 - JavaScript with types
JavaScript:
function greet(name) {
return "Hello, " + name.toUpperCase();
}
greet("John"); // "Hello, JOHN"
greet(42); // Runtime error: name.toUpperCase is not a function
TypeScript:
function greet(name: string): string {
return "Hello, " + name.toUpperCase();
}
greet("John"); // "Hello, JOHN"
greet(42); // Compile-time error: Argument of type 'number' is not assignable to parameter of type 'string'
Interfaces:
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional
}
function createUser(user: User): void {
console.log(`Creating user: ${user.name}`);
}
createUser({ id: 1, name: "John", email: "john@example.com" }); // OK
createUser({ id: 1, name: "John" }); // Error: Property 'email' is missing
Classes (before ES6):
class Animal {
private name: string;
constructor(name: string) {
this.name = name;
}
public move(distance: number): void {
console.log(`${this.name} moved ${distance}m.`);
}
}
class Dog extends Animal {
bark(): void {
console.log("Woof! Woof!");
}
}
const dog = new Dog("Buddy");
dog.bark();
dog.move(10);
2013-2014: Build Tools & Module Bundlers
Grunt (2012) - Task runner
// Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
uglify: {
build: {
src: 'src/*.js',
dest: 'build/app.min.js'
}
},
watch: {
files: ['src/*.js'],
tasks: ['uglify']
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('default', ['uglify']);
};
Gulp (2013) - Streaming build system
// gulpfile.js
var gulp = require('gulp');
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');
gulp.task('scripts', function() {
return gulp.src('src/*.js')
.pipe(concat('app.js'))
.pipe(uglify())
.pipe(gulp.dest('build'));
});
gulp.task('watch', function() {
gulp.watch('src/*.js', ['scripts']);
});
gulp.task('default', ['scripts', 'watch']);
Browserify (2011) - Bring Node.js modules to browser
// Before: Multiple script tags
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="app.js"></script>
// With Browserify
// app.js
var $ = require('jquery');
var _ = require('lodash');
// Build
browserify app.js -o bundle.js
// HTML
<script src="bundle.js"></script>
Webpack (2012, popular 2014+) - Module bundler
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
The Renaissance: ES6/ES2015 (2015)
The biggest update to JavaScript ever. Changed the language fundamentally.
let and const
Before (var):
// Function scope, hoisting issues
function example() {
console.log(x); // undefined (hoisted)
var x = 10;
if (true) {
var x = 20; // Same variable!
console.log(x); // 20
}
console.log(x); // 20
}
// Loop problem
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3 (all reference same i)
}, 100);
}
After (let/const):
// Block scope, no hoisting
function example() {
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
if (true) {
let x = 20; // Different variable!
console.log(x); // 20
}
console.log(x); // 10
}
// Loop fixed
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2 (each has own i)
}, 100);
}
// const - immutable binding
const PI = 3.14159;
PI = 3; // TypeError: Assignment to constant variable
const obj = { name: "John" };
obj.name = "Jane"; // OK - object is mutable
obj = {}; // TypeError: Assignment to constant variable
Arrow Functions
Before:
// Function expressions
var add = function(a, b) {
return a + b;
};
// this binding issues
var obj = {
name: "John",
friends: ["Jane", "Bob"],
printFriends: function() {
var self = this; // Workaround
this.friends.forEach(function(friend) {
console.log(self.name + " knows " + friend);
});
}
};
After:
// Arrow functions
const add = (a, b) => a + b;
// Implicit return
const square = x => x * x;
// Multiple statements
const greet = name => {
const message = `Hello, ${name}`;
return message.toUpperCase();
};
// Lexical this
const obj = {
name: "John",
friends: ["Jane", "Bob"],
printFriends() {
this.friends.forEach(friend => {
console.log(`${this.name} knows ${friend}`); // this works!
});
}
};
// Array methods become elegant
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
Template Literals
Before:
var name = "John";
var age = 30;
var message = "Hello, my name is " + name + " and I am " + age + " years old.";
// Multiline strings
var html = "<div>\n" +
" <h1>" + title + "</h1>\n" +
" <p>" + content + "</p>\n" +
"</div>";
After:
const name = "John";
const age = 30;
const message = `Hello, my name is ${name} and I am ${age} years old.`;
// Multiline strings
const html = `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
// Expressions
const price = 19.99;
const tax = 0.08;
const total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] ? `<mark>${values[i]}</mark>` : '');
}, '');
}
const name = "John";
const html = highlight`Hello, ${name}!`; // "Hello, <mark>John</mark>!"
Destructuring
Before:
var user = { name: "John", age: 30, email: "john@example.com" };
var name = user.name;
var age = user.age;
var numbers = [1, 2, 3, 4, 5];
var first = numbers[0];
var second = numbers[1];
function getUser() {
return { name: "John", age: 30 };
}
var user = getUser();
var name = user.name;
After:
// Object destructuring
const user = { name: "John", age: 30, email: "john@example.com" };
const { name, age } = user;
// Renaming
const { name: userName, age: userAge } = user;
// Default values
const { name, age, country = "USA" } = user;
// Nested destructuring
const user = {
name: "John",
address: {
city: "New York",
country: "USA"
}
};
const { address: { city, country } } = user;
// Array destructuring
const numbers = [1, 2, 3, 4, 5];
const [first, second] = numbers; // 1, 2
const [first, , third] = numbers; // 1, 3
const [first, ...rest] = numbers; // 1, [2, 3, 4, 5]
// Function parameters
function greet({ name, age }) {
console.log(`Hello, ${name}. You are ${age} years old.`);
}
greet({ name: "John", age: 30 });
// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a]; // a = 2, b = 1
Default Parameters
Before:
function greet(name, greeting) {
name = name || "Guest";
greeting = greeting || "Hello";
return greeting + ", " + name;
}
// Problem with falsy values
greet("", "Hi"); // "Hi, Guest" - empty string is falsy!
After:
function greet(name = "Guest", greeting = "Hello") {
return `${greeting}, ${name}`;
}
greet(); // "Hello, Guest"
greet("John"); // "Hello, John"
greet("John", "Hi"); // "Hi, John"
greet("", "Hi"); // "Hi, " - empty string is preserved
// Default can be expressions
function createUser(name, id = Date.now()) {
return { name, id };
}
// Can reference earlier parameters
function greet(name, greeting = `Hello, ${name}`) {
return greeting;
}
Rest and Spread Operators
Before:
// Rest parameters
function sum() {
var args = Array.prototype.slice.call(arguments);
return args.reduce(function(acc, n) {
return acc + n;
}, 0);
}
// Combining arrays
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var combined = arr1.concat(arr2);
// Copying arrays
var original = [1, 2, 3];
var copy = original.slice();
After:
// Rest parameters
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
function greet(greeting, ...names) {
return `${greeting}, ${names.join(" and ")}!`;
}
greet("Hello", "John", "Jane", "Bob"); // "Hello, John and Jane and Bob!"
// Spread operator - arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const copy = [...original];
// Spread operator - objects (ES2018)
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combined = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// Overriding properties
const defaults = { theme: "light", lang: "en" };
const userPrefs = { theme: "dark" };
const settings = { ...defaults, ...userPrefs }; // { theme: "dark", lang: "en" }
// Function calls
const numbers = [1, 5, 3, 9, 2];
Math.max(...numbers); // 9
Classes
Before (Prototypes):
// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return "Hello, I'm " + this.name;
};
Person.prototype.haveBirthday = function() {
this.age++;
};
// Inheritance
function Employee(name, age, title) {
Person.call(this, name, age);
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.work = function() {
return this.name + " is working as " + this.title;
};
var emp = new Employee("John", 30, "Developer");
emp.greet(); // "Hello, I'm John"
emp.work(); // "John is working as Developer"
After (Classes):
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
haveBirthday() {
this.age++;
}
// Static methods
static species() {
return "Homo sapiens";
}
}
class Employee extends Person {
constructor(name, age, title) {
super(name, age); // Call parent constructor
this.title = title;
}
work() {
return `${this.name} is working as ${this.title}`;
}
// Override parent method
greet() {
return `${super.greet()}, I'm a ${this.title}`;
}
}
const emp = new Employee("John", 30, "Developer");
emp.greet(); // "Hello, I'm John, I'm a Developer"
emp.work(); // "John is working as Developer"
Person.species(); // "Homo sapiens"
// Getters and setters
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
set area(value) {
this.width = Math.sqrt(value);
this.height = Math.sqrt(value);
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area); // 50
rect.area = 100;
console.log(rect.width); // 10
Promises
Before (Callback Hell):
// Pyramid of doom
getUserData(userId, function(err, user) {
if (err) {
handleError(err);
} else {
getOrders(user.id, function(err, orders) {
if (err) {
handleError(err);
} else {
getOrderDetails(orders[0].id, function(err, details) {
if (err) {
handleError(err);
} else {
displayDetails(details);
}
});
}
});
}
});
After (Promises):
// Promise chain
getUserData(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => displayDetails(details))
.catch(err => handleError(err));
// Creating promises
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}
delay(1000).then(() => console.log("1 second passed"));
// Promise.all - parallel execution
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(([users, posts, comments]) => {
console.log("All data loaded");
})
.catch(err => console.error("One failed:", err));
// Promise.race - first to complete
Promise.race([
fetch('/api/fast'),
fetch('/api/slow')
])
.then(result => console.log("First one won:", result));
// Error handling
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return response.json();
})
.then(data => processData(data))
.catch(err => console.error('Failed:', err))
.finally(() => console.log('Cleanup'));
Modules (ES6 Modules)
Before (CommonJS - Node.js):
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// app.js
var math = require('./math');
console.log(math.add(5, 3));
Before (Browser - Script tags):
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
<!-- Global namespace pollution, order matters -->
After (ES6 Modules):
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// or
export { add, subtract };
// Default export
export default function multiply(a, b) {
return a * b;
}
// app.js
import multiply, { add, subtract } from './math.js';
console.log(add(5, 3)); // 8
console.log(multiply(5, 3)); // 15
// Import everything
import * as math from './math.js';
console.log(math.add(5, 3));
// Rename imports
import { add as sum } from './math.js';
// Re-export
export { add, subtract } from './math.js';
export * from './math.js';
Other ES6 Features
Map and Set:
// Before: Objects as maps (problems with keys)
var map = {};
map['key1'] = 'value1';
map[{}] = 'value2'; // [object Object] as key - not what you want!
// ES6 Map
const map = new Map();
map.set('key1', 'value1');
map.set({}, 'value2'); // Objects as keys work!
map.set(function() {}, 'value3'); // Functions as keys!
map.get('key1'); // 'value1'
map.has('key1'); // true
map.delete('key1');
map.size; // 2
// Iteration
for (const [key, value] of map) {
console.log(key, value);
}
// Set - unique values
const set = new Set([1, 2, 3, 3, 4, 4, 5]);
console.log(set); // Set { 1, 2, 3, 4, 5 }
set.add(6);
set.has(3); // true
set.delete(3);
set.size; // 5
// Array deduplication
const numbers = [1, 2, 3, 3, 4, 4, 5];
const unique = [...new Set(numbers)]; // [1, 2, 3, 4, 5]
Symbols:
// Unique identifiers
const sym1 = Symbol('description');
const sym2 = Symbol('description');
sym1 === sym2; // false - always unique
// Private-ish object properties
const _private = Symbol('private');
class MyClass {
constructor() {
this[_private] = 'secret';
this.public = 'visible';
}
getPrivate() {
return this[_private];
}
}
const obj = new MyClass();
console.log(obj.public); // 'visible'
console.log(obj[_private]); // undefined (need the symbol reference)
Object.keys(obj); // ['public'] - symbol not enumerated
Iterators and Generators:
// Iterator
const iterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
for (const value of iterable) {
console.log(value); // 1, 2, 3, 4, 5
}
// Generator - easier way
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { done: true }
// Infinite sequences
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
fib.next().value; // 2
fib.next().value; // 3
// Practical use - lazy evaluation
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for (const num of range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
2013: React.js
Facebook releases React - Component-based UI library
Before React (jQuery):
// Imperative, manual DOM manipulation
var todos = [];
function addTodo(text) {
todos.push({ id: Date.now(), text: text, done: false });
renderTodos();
}
function toggleTodo(id) {
var todo = todos.find(function(t) { return t.id === id; });
todo.done = !todo.done;
renderTodos();
}
function renderTodos() {
var html = todos.map(function(todo) {
return '<li>' +
'<input type="checkbox" ' + (todo.done ? 'checked' : '') + ' data-id="' + todo.id + '">' +
'<span>' + todo.text + '</span>' +
'</li>';
}).join('');
$('#todos').html(html);
}
$(document).on('change', 'input[type=checkbox]', function() {
toggleTodo($(this).data('id'));
});
With React:
// Declarative, component-based
class TodoApp extends React.Component {
state = {
todos: [],
input: ''
};
addTodo = () => {
this.setState({
todos: [...this.state.todos, {
id: Date.now(),
text: this.state.input,
done: false
}],
input: ''
});
};
toggleTodo = (id) => {
this.setState({
todos: this.state.todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
});
};
render() {
return (
<div>
<input
value={this.state.input}
onChange={e => this.setState({ input: e.target.value })}
/>
<button onClick={this.addTodo}>Add</button>
<ul>
{this.state.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => this.toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
</div>
);
}
}
JSX - HTML in JavaScript:
// JSX (transpiled by Babel)
const element = <h1>Hello, {name}!</h1>;
// Compiles to
const element = React.createElement('h1', null, 'Hello, ', name, '!');
// Components
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// Usage
<Welcome name="John" />
2014: Vue.js
Evan You releases Vue.js - Progressive framework
// Vue instance
new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
todos: [],
newTodo: ''
},
methods: {
addTodo() {
this.todos.push({
id: Date.now(),
text: this.newTodo,
done: false
});
this.newTodo = '';
},
toggleTodo(todo) {
todo.done = !todo.done;
}
}
});
<div id="app">
<input v-model="newTodo" @keyup.enter="addTodo">
<button @click="addTodo">Add</button>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
</li>
</ul>
</div>
Annual Releases: ES2016-ES2025
ES2016 (ES7)
Array.prototype.includes:
// Before
const numbers = [1, 2, 3, 4, 5];
numbers.indexOf(3) !== -1; // true
numbers.indexOf(6) !== -1; // false
// Problem with NaN
[1, 2, NaN].indexOf(NaN); // -1 (doesn't work!)
// After
numbers.includes(3); // true
numbers.includes(6); // false
[1, 2, NaN].includes(NaN); // true (works!)
Exponentiation Operator:
// Before
Math.pow(2, 3); // 8
// After
2 ** 3; // 8
2 ** 3 ** 2; // 512 (right-associative: 2 ** (3 ** 2))
// With assignment
let num = 2;
num **= 3; // 8
ES2017 (ES8)
async/await - Game changer for asynchronous code
Before (Promises):
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/orders/${user.id}`)
.then(response => response.json())
.then(orders => {
return fetch(`/api/details/${orders[0].id}`)
.then(response => response.json())
.then(details => {
return { user, orders, details };
});
});
})
.catch(err => console.error(err));
}
After (async/await):
async function fetchUserData(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const ordersResponse = await fetch(`/api/orders/${user.id}`);
const orders = await ordersResponse.json();
const detailsResponse = await fetch(`/api/details/${orders[0].id}`);
const details = await detailsResponse.json();
return { user, orders, details };
} catch (err) {
console.error(err);
}
}
// Parallel execution
async function fetchAllData() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
}
// Error handling
async function getData() {
try {
const data = await fetch('/api/data');
return await data.json();
} catch (err) {
console.error('Failed:', err);
throw err;
} finally {
console.log('Cleanup');
}
}
// Top-level await (ES2022)
const data = await fetch('/api/data').then(r => r.json());
Object.entries() and Object.values():
const obj = { a: 1, b: 2, c: 3 };
// Before
Object.keys(obj); // ['a', 'b', 'c']
Object.keys(obj).map(key => obj[key]); // [1, 2, 3]
// After
Object.values(obj); // [1, 2, 3]
Object.entries(obj); // [['a', 1], ['b', 2], ['c', 3]]
// Practical use
for (const [key, value] of Object.entries(obj)) {
console.log(`${key}: ${value}`);
}
// Convert to Map
const map = new Map(Object.entries(obj));
// Convert from Map
const obj = Object.fromEntries(map);
String padding:
'5'.padStart(3, '0'); // '005'
'5'.padEnd(3, '0'); // '500'
// Practical use
const numbers = [1, 10, 100];
numbers.forEach(n => {
console.log(n.toString().padStart(3, '0'));
});
// 001
// 010
// 100
Object.getOwnPropertyDescriptors():
const obj = {
get foo() { return 'bar'; }
};
// Shallow clone with getters/setters
const clone = Object.defineProperties(
{},
Object.getOwnPropertyDescriptors(obj)
);
ES2018 (ES9)
Rest/Spread for Objects:
// Rest
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 };
console.log(rest); // { c: 3, d: 4 }
// Spread
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// Overriding
const defaults = { theme: 'light', lang: 'en' };
const userPrefs = { theme: 'dark' };
const settings = { ...defaults, ...userPrefs }; // { theme: 'dark', lang: 'en' }
// Shallow clone
const clone = { ...original };
// Remove property
const { password, ...userWithoutPassword } = user;
Async Iteration:
// Async iterator
const asyncIterable = {
async *[Symbol.asyncIterator]() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
};
for await (const num of asyncIterable) {
console.log(num); // 1, 2, 3
}
// Practical use - streaming data
async function* fetchPages(url) {
let page = 1;
while (true) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const pageData of fetchPages('/api/items')) {
console.log(pageData);
}
Promise.finally():
fetch('/api/data')
.then(response => response.json())
.then(data => processData(data))
.catch(err => handleError(err))
.finally(() => {
hideLoadingSpinner(); // Always runs
});
RegExp improvements:
// Named capture groups
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = pattern.exec('2025-12-04');
console.log(match.groups.year); // '2025'
console.log(match.groups.month); // '12'
console.log(match.groups.day); // '04'
// Lookbehind assertions
/(?<=\$)\d+/.test('$100'); // true
/(?<!\$)\d+/.test('100'); // true
ES2019 (ES10)
Array.prototype.flat() and flatMap():
// Before - flatten nested arrays
const nested = [1, [2, 3], [4, [5, 6]]];
const flat = nested.reduce((acc, val) =>
acc.concat(Array.isArray(val) ? val : [val]), []
); // [1, 2, 3, 4, [5, 6]]
// After
nested.flat(); // [1, 2, 3, 4, [5, 6]]
nested.flat(2); // [1, 2, 3, 4, 5, 6] - depth 2
nested.flat(Infinity); // Flatten all levels
// flatMap - map then flatten
const arr = [1, 2, 3];
arr.map(x => [x, x * 2]); // [[1, 2], [2, 4], [3, 6]]
arr.flatMap(x => [x, x * 2]); // [1, 2, 2, 4, 3, 6]
// Practical use
const sentences = ['Hello world', 'How are you'];
sentences.flatMap(s => s.split(' ')); // ['Hello', 'world', 'How', 'are', 'you']
Object.fromEntries():
// Convert array of entries to object
const entries = [['a', 1], ['b', 2], ['c', 3]];
const obj = Object.fromEntries(entries); // { a: 1, b: 2, c: 3 }
// Convert Map to object
const map = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(map);
// Transform object
const obj = { a: 1, b: 2, c: 3 };
const doubled = Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, value * 2])
); // { a: 2, b: 4, c: 6 }
// Filter object
const filtered = Object.fromEntries(
Object.entries(obj).filter(([key, value]) => value > 1)
); // { b: 2, c: 3 }
String.prototype.trimStart() and trimEnd():
' hello '.trimStart(); // 'hello '
' hello '.trimEnd(); // ' hello'
' hello '.trim(); // 'hello'
Optional catch binding:
// Before
try {
JSON.parse(data);
} catch (err) { // Had to specify err even if not used
console.log('Invalid JSON');
}
// After
try {
JSON.parse(data);
} catch {
console.log('Invalid JSON');
}
ES2020 (ES11)
Optional Chaining (?.):
// Before
const city = user && user.address && user.address.city;
if (user && user.address && user.address.city) {
console.log(user.address.city);
}
// After
const city = user?.address?.city;
if (user?.address?.city) {
console.log(user.address.city);
}
// With arrays
const firstItem = arr?.[0];
// With functions
const result = obj.method?.();
// Practical examples
const userName = response?.data?.user?.name ?? 'Guest';
const onClick = props?.onClick?.();
Nullish Coalescing (??):
// Before - problem with falsy values
const value = input || 'default';
// Problem: 0, '', false are all replaced with 'default'
const count = 0;
const display = count || 'No items'; // 'No items' - wrong!
// After - only null/undefined
const value = input ?? 'default';
const count = 0;
const display = count ?? 'No items'; // 0 - correct!
const name = '';
const displayName = name ?? 'Anonymous'; // '' - empty string is kept
// Combined with optional chaining
const userName = user?.name ?? 'Guest';
BigInt - Arbitrary precision integers:
// Before - limited to Number.MAX_SAFE_INTEGER
const big = 9007199254740991; // MAX_SAFE_INTEGER
big + 1; // 9007199254740992
big + 2; // 9007199254740992 - wrong!
// After
const bigInt = 9007199254740991n;
bigInt + 1n; // 9007199254740992n
bigInt + 2n; // 9007199254740993n - correct!
// Or
const bigInt = BigInt('9007199254740991');
// Operations
10n + 20n; // 30n
10n * 20n; // 200n
10n / 3n; // 3n (truncates)
// Cannot mix with Number
10n + 5; // TypeError
10n + BigInt(5); // 15n
Promise.allSettled():
// Promise.all - fails if any fails
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(results => console.log('All succeeded'))
.catch(err => console.log('One failed')); // Stops on first failure
// Promise.allSettled - waits for all
Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
});
// Result format
[
{ status: 'fulfilled', value: response1 },
{ status: 'rejected', reason: error },
{ status: 'fulfilled', value: response3 }
]
globalThis:
// Before - different in different environments
// Browser: window
// Node.js: global
// Web Workers: self
// After - works everywhere
globalThis.setTimeout(() => {}, 1000);
Dynamic import():
// Before - static imports only
import { heavy } from './heavy-module.js'; // Loaded immediately
// After - dynamic imports
button.addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.doSomething();
});
// Conditional loading
if (condition) {
const module = await import('./module-a.js');
} else {
const module = await import('./module-b.js');
}
// Practical use - code splitting
async function loadChart() {
const { Chart } = await import('chart.js');
return new Chart(ctx, config);
}
ES2021 (ES12)
String.prototype.replaceAll():
// Before
const str = 'hello world, hello universe';
str.replace(/hello/g, 'hi'); // 'hi world, hi universe'
str.split('hello').join('hi'); // Workaround
// After
str.replaceAll('hello', 'hi'); // 'hi world, hi universe'
Logical Assignment Operators:
// Before
if (!obj.prop) {
obj.prop = 'default';
}
// After
obj.prop ||= 'default';
// Nullish coalescing assignment
obj.prop ??= 'default'; // Only if null/undefined
// AND assignment
obj.prop &&= newValue; // Only if truthy
// Practical examples
let count = 0;
count ||= 10; // count = 10
count ||= 20; // count = 10 (already truthy)
let value = null;
value ??= 'default'; // value = 'default'
value ??= 'other'; // value = 'default' (not null)
Numeric Separators:
// Before
const billion = 1000000000;
const bytes = 0b11111111;
// After - more readable
const billion = 1_000_000_000;
const bytes = 0b1111_1111;
const hex = 0xFF_FF_FF_FF;
const price = 1_234_567.89;
Promise.any():
// Returns first fulfilled promise
Promise.any([
fetch('/api/server1'),
fetch('/api/server2'),
fetch('/api/server3')
])
.then(response => console.log('First success:', response))
.catch(err => console.log('All failed'));
// vs Promise.race (returns first settled, even if rejected)
Promise.race([...]) // First to complete (success or failure)
Promise.any([...]) // First to succeed
WeakRef and FinalizationRegistry:
// WeakRef - weak reference to object
const obj = { data: 'important' };
const weakRef = new WeakRef(obj);
// Later
const obj = weakRef.deref();
if (obj) {
console.log(obj.data);
} else {
console.log('Object was garbage collected');
}
// FinalizationRegistry - cleanup callback
const registry = new FinalizationRegistry((value) => {
console.log(`Cleanup: ${value}`);
});
let obj = { data: 'test' };
registry.register(obj, 'my-object');
obj = null; // Eventually triggers cleanup callback
ES2022 (ES13)
Top-level await:
// Before - had to wrap in async function
(async () => {
const data = await fetch('/api/data');
console.log(data);
})();
// After - in modules
const data = await fetch('/api/data');
console.log(data);
// Practical use
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
export { users, posts };
Class Fields and Private Methods:
// Before
class Counter {
constructor() {
this.count = 0;
this._private = 'secret'; // Convention, not enforced
}
}
// After - public fields
class Counter {
count = 0; // Public field
#private = 'secret'; // Private field
increment() {
this.count++;
}
#privateMethod() { // Private method
return this.#private;
}
getPrivate() {
return this.#privateMethod();
}
}
const counter = new Counter();
counter.count; // 0
counter.#private; // SyntaxError
counter.#privateMethod(); // SyntaxError
// Static fields and methods
class MathUtils {
static PI = 3.14159;
static #secret = 'private static';
static double(n) {
return n * 2;
}
static #privateStatic() {
return this.#secret;
}
}
MathUtils.PI; // 3.14159
MathUtils.double(5); // 10
// Static initialization blocks
class Database {
static #connection;
static {
// Runs once when class is defined
this.#connection = createConnection();
}
}
Array.prototype.at():
const arr = [1, 2, 3, 4, 5];
// Before - negative indexing
arr[arr.length - 1]; // 5
arr[arr.length - 2]; // 4
// After
arr.at(-1); // 5
arr.at(-2); // 4
arr.at(0); // 1
// Works with strings too
'hello'.at(-1); // 'o'
Object.hasOwn():
const obj = { prop: 'value' };
// Before
obj.hasOwnProperty('prop'); // true
// Problem: can be overridden
obj.hasOwnProperty = () => false;
obj.hasOwnProperty('prop'); // false - wrong!
// After
Object.hasOwn(obj, 'prop'); // true - always works
Error.cause:
// Before
try {
await fetch('/api/data');
} catch (err) {
throw new Error('Failed to fetch data'); // Lost original error
}
// After
try {
await fetch('/api/data');
} catch (err) {
throw new Error('Failed to fetch data', { cause: err });
}
// Access original error
catch (err) {
console.log(err.message); // 'Failed to fetch data'
console.log(err.cause); // Original fetch error
}
RegExp Match Indices:
const pattern = /test(\d+)/d; // Note the 'd' flag
const match = pattern.exec('test123');
match.indices; // [[0, 7], [4, 7]]
// [0, 7] - full match position
// [4, 7] - first capture group position
ES2023 (ES14)
Array methods: findLast(), findLastIndex():
const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];
// Before - find from start
arr.find(x => x > 3); // 4 (first occurrence)
arr.findIndex(x => x > 3); // 3
// After - find from end
arr.findLast(x => x > 3); // 4 (last occurrence)
arr.findLastIndex(x => x > 3); // 5
// Practical use
const logs = [
{ level: 'info', msg: 'Started' },
{ level: 'error', msg: 'Failed' },
{ level: 'info', msg: 'Retry' },
{ level: 'error', msg: 'Failed again' }
];
const lastError = logs.findLast(log => log.level === 'error');
// { level: 'error', msg: 'Failed again' }
Array.prototype.toSorted(), toReversed(), toSpliced(), with():
const arr = [3, 1, 4, 1, 5];
// Before - mutating methods
arr.sort(); // Modifies arr
arr.reverse(); // Modifies arr
arr.splice(1, 2); // Modifies arr
// After - non-mutating methods
const sorted = arr.toSorted(); // Returns new array
const reversed = arr.toReversed(); // Returns new array
const spliced = arr.toSpliced(1, 2); // Returns new array
const modified = arr.with(2, 99); // Returns new array with arr[2] = 99
// Original array unchanged
console.log(arr); // [3, 1, 4, 1, 5]
// Practical use - immutable updates
const state = [1, 2, 3, 4, 5];
const newState = state.with(2, 99); // [1, 2, 99, 4, 5]
Hashbang Grammar:
#!/usr/bin/env node
// Now valid JavaScript syntax
console.log('Hello from script');
Symbols as WeakMap keys:
const weak = new WeakMap();
const sym = Symbol('key');
weak.set(sym, 'value'); // Now allowed
ES2024 (ES15)
Array Grouping:
const items = [
{ type: 'fruit', name: 'apple' },
{ type: 'vegetable', name: 'carrot' },
{ type: 'fruit', name: 'banana' },
{ type: 'vegetable', name: 'broccoli' }
];
// Before
const grouped = items.reduce((acc, item) => {
if (!acc[item.type]) acc[item.type] = [];
acc[item.type].push(item);
return acc;
}, {});
// After
const grouped = Object.groupBy(items, item => item.type);
// {
// fruit: [{ type: 'fruit', name: 'apple' }, { type: 'fruit', name: 'banana' }],
// vegetable: [{ type: 'vegetable', name: 'carrot' }, { type: 'vegetable', name: 'broccoli' }]
// }
// Map.groupBy for Map result
const groupedMap = Map.groupBy(items, item => item.type);
Promise.withResolvers():
// Before
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// After
const { promise, resolve, reject } = Promise.withResolvers();
// Practical use
class AsyncQueue {
#queue = [];
async dequeue() {
if (this.#queue.length > 0) {
return this.#queue.shift();
}
const { promise, resolve } = Promise.withResolvers();
this.#resolvers.push(resolve);
return promise;
}
}
Atomics.waitAsync():
// Non-blocking wait on SharedArrayBuffer
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);
// Before - blocking
Atomics.wait(view, 0, 0); // Blocks thread
// After - non-blocking
const result = Atomics.waitAsync(view, 0, 0);
result.value.then(() => {
console.log('Value changed');
});
ES2025 (Current)
Duplicate named capture groups in RegExp:
// Now allowed
const pattern = /(?<year>\d{4})-\d{2}|\d{2}-(?<year>\d{4})/;
const match1 = pattern.exec('2025-12');
const match2 = pattern.exec('12-2025');
match1.groups.year; // '2025'
match2.groups.year; // '2025'
Set methods:
const set1 = new Set([1, 2, 3]);
const set2 = new Set([3, 4, 5]);
// Union
set1.union(set2); // Set {1, 2, 3, 4, 5}
// Intersection
set1.intersection(set2); // Set {3}
// Difference
set1.difference(set2); // Set {1, 2}
// Symmetric difference
set1.symmetricDifference(set2); // Set {1, 2, 4, 5}
// Subset/superset
set1.isSubsetOf(set2); // false
set1.isDisjointFrom(set2); // false
TypeScript Evolution (2012-2025)
Created: 2012 by Microsoft (Anders Hejlsberg - creator of C#)
Problem it solved:
- 🎯 JavaScript had no type safety (runtime errors everywhere)
- 🎯 Large codebases were unmaintainable (refactoring was dangerous)
- 🎯 No IDE autocomplete/IntelliSense (guessing API)
- 🎯 Bugs only found at runtime (no compile-time checks)
- 🎯 Documentation was comments (no enforced contracts)
The breakthrough:
- ✨ Static typing - Catch errors before running code
- ✨ IntelliSense - IDE knows what properties/methods exist
- ✨ Refactoring - Rename safely across entire codebase
- ✨ Compiles to JavaScript - Works everywhere JS works
- ✨ Gradual adoption - Can add types incrementally
// JavaScript - Runtime errors
function getUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
const user = getUser(123);
console.log(user.name); // Error! user is a Promise, not an object
// Only discover this when running the code
// TypeScript - Compile-time errors
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
const user = getUser(123);
console.log(user.name); // Error! Type 'Promise<User>' has no property 'name'
// IDE shows error immediately, before running
const user = await getUser(123);
console.log(user.name); // ✅ Works! IDE autocompletes 'name'
TypeScript 1.0 (2014)
Basic Types:
// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];
// Tuples
let tuple: [string, number] = ["John", 30];
// Enums
enum Color {
Red,
Green,
Blue
}
let color: Color = Color.Red;
// Any - escape hatch
let anything: any = 4;
anything = "string";
anything = true;
// Functions
function add(a: number, b: number): number {
return a + b;
}
const multiply = (a: number, b: number): number => a * b;
Interfaces:
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional
readonly created: Date; // Readonly
}
function createUser(user: User): void {
console.log(user.name);
}
// Extending interfaces
interface Admin extends User {
role: string;
permissions: string[];
}
TypeScript 1.4 (2015) - Union Types
// Union types
let value: string | number;
value = "hello";
value = 42;
// Type guards
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // TypeScript knows it's string
} else {
return value.toFixed(2); // TypeScript knows it's number
}
}
// Literal types
let direction: "north" | "south" | "east" | "west";
direction = "north"; // OK
direction = "up"; // Error
TypeScript 1.6 (2015) - Intersection Types
// Intersection types
interface Loggable {
log(): void;
}
interface Serializable {
serialize(): string;
}
type LoggableSerializable = Loggable & Serializable;
class MyClass implements LoggableSerializable {
log() { console.log("logging"); }
serialize() { return "{}"; }
}
TypeScript 2.0 (2016) - Major Update
Non-nullable types:
// Before - everything could be null/undefined
let name: string = null; // OK
// After - strictNullChecks
let name: string = null; // Error
let name: string | null = null; // OK
function greet(name: string) {
console.log(name.toUpperCase()); // Safe - name cannot be null
}
function greet(name: string | null) {
if (name) {
console.log(name.toUpperCase()); // Must check first
}
}
Readonly properties:
interface Point {
readonly x: number;
readonly y: number;
}
let point: Point = { x: 10, y: 20 };
point.x = 5; // Error
// Readonly arrays
let arr: ReadonlyArray<number> = [1, 2, 3];
arr.push(4); // Error
arr[0] = 5; // Error
Tagged union types:
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function area(shape: Shape): number {
switch (shape.kind) {
case "square":
return shape.size ** 2; // TypeScript knows it's Square
case "rectangle":
return shape.width * shape.height; // Rectangle
case "circle":
return Math.PI * shape.radius ** 2; // Circle
}
}
TypeScript 2.1 (2016) - Advanced Features
Mapped types:
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>; // { name?: string; age?: number; }
type ReadonlyUser = Readonly<User>; // { readonly name: string; readonly age: number; }
keyof and lookup types:
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // "name" | "age"
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "John", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
TypeScript 2.8 (2018) - Conditional Types
// Conditional types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical use - extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { name: "John", age: 30 };
}
type User = ReturnType<typeof getUser>; // { name: string; age: number; }
// Exclude/Extract
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
TypeScript 3.0 (2018) - Tuples and Rest
Tuple improvements:
// Rest elements in tuples
type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];
// [string, string, number, number, boolean]
// Optional elements
type OptionalTuple = [string, number?];
let t1: OptionalTuple = ["hello"];
let t2: OptionalTuple = ["hello", 42];
TypeScript 3.7 (2019) - Optional Chaining
// Optional chaining (same as JavaScript)
const city = user?.address?.city;
// Nullish coalescing
const name = user?.name ?? "Guest";
// Assertion functions
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg);
}
}
function processValue(value: string | null) {
assert(value !== null, "Value must not be null");
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
TypeScript 4.0 (2020) - Variadic Tuples
// Variadic tuple types
function concat<T extends unknown[], U extends unknown[]>(
arr1: T,
arr2: U
): [...T, ...U] {
return [...arr1, ...arr2];
}
const result = concat([1, 2], ["a", "b"]);
// type: [number, number, string, string]
// Labeled tuple elements
type Range = [start: number, end: number];
function createRange(...[start, end]: Range) {
// ...
}
TypeScript 4.1 (2020) - Template Literal Types
// Template literal types
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// "one fish" | "two fish" | "red fish" | "blue fish"
// Practical use - API routes
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = "/users" | "/posts" | "/comments";
type APIRoute = `${HTTPMethod} ${Route}`;
// "GET /users" | "POST /users" | ... (12 combinations)
// Intrinsic string manipulation
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
type Loud = Uppercase<"hello">; // "HELLO"
type Quiet = Lowercase<"HELLO">; // "hello"
Recursive conditional types:
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface Nested {
a: {
b: {
c: number;
};
};
}
type ReadonlyNested = DeepReadonly<Nested>;
// All levels are readonly
TypeScript 4.5 (2021) - Awaited Type
// Awaited - unwrap Promise types
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
// Practical use
async function fetchUser() {
return { name: "John", age: 30 };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number; }
TypeScript 4.7 (2022) - Module Detection
ES Module support improvements:
// package.json
{
"type": "module"
}
// Now .ts files are treated as ES modules by default
TypeScript 4.9 (2022) - satisfies Operator
// Before - lose type information
const config: Record<string, string | number> = {
host: "localhost",
port: 3000
};
config.host.toUpperCase(); // Error - might be number
// After - keep specific types
const config = {
host: "localhost",
port: 3000
} satisfies Record<string, string | number>;
config.host.toUpperCase(); // OK - TypeScript knows it's string
config.port.toFixed(2); // OK - TypeScript knows it's number
TypeScript 5.0 (2023) - Decorators
Standard decorators:
// Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class MyClass {
// ...
}
// Method decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return original.apply(this, args);
};
}
class Calculator {
@log
add(a: number, b: number) {
return a + b;
}
}
const type parameters:
// Infer literal types
function makeArray<const T>(arr: T[]): T[] {
return arr;
}
const arr1 = makeArray([1, 2, 3]); // number[]
const arr2 = makeArray([1, 2, 3] as const); // readonly [1, 2, 3]
TypeScript 5.2 (2023) - using Declarations
// Explicit resource management
{
using file = openFile("data.txt");
// file is automatically closed at end of scope
}
// Async disposal
{
await using connection = await connectToDatabase();
// connection is automatically closed
}
TypeScript 5.4 (2024) - NoInfer
// Prevent type inference in specific places
function createStore<T>(
initial: T,
updater: (current: NoInfer<T>) => T
) {
// ...
}
// Forces updater parameter to match initial type exactly
TypeScript 5.5 (2024) - Inferred Type Predicates
// Before - manual type predicate
function isString(value: unknown): value is string {
return typeof value === "string";
}
// After - inferred
function isString(value: unknown) {
return typeof value === "string";
}
// TypeScript infers the type predicate automatically
Server-Side JavaScript: Node.js & Beyond
Created: 2009 by Ryan Dahl
Problem it solved:
- 🎯 JavaScript was only for browsers (couldn't build servers)
- 🎯 Apache/PHP blocked on I/O (one thread per request = slow)
- 🎯 Couldn't use same language for frontend and backend
- 🎯 Needed non-blocking I/O for real-time apps (chat, streaming)
- 🎯 C10K problem (handling 10,000 concurrent connections)
The breakthrough:
- ✨ V8 engine - Fast JavaScript execution (from Chrome)
- ✨ Event loop - Non-blocking I/O (handle thousands of connections)
- ✨ Single language - JavaScript everywhere (frontend + backend)
- ✨ npm - Largest package ecosystem ever created
- ✨ Async by default - Perfect for I/O-heavy applications
// Before Node.js - PHP (blocking)
<?php
// Each request blocks a thread
$data = file_get_contents('file.txt'); // Blocks!
$result = mysql_query("SELECT * FROM users"); // Blocks!
echo $data;
?>
// Apache: 1 thread per request = max ~1000 concurrent users
// With Node.js - Non-blocking
const fs = require('fs');
const mysql = require('mysql');
// Handles thousands of concurrent requests
fs.readFile('file.txt', (err, data) => {
// Doesn't block! Event loop continues
console.log(data);
});
mysql.query("SELECT * FROM users", (err, results) => {
// Doesn't block! Event loop continues
console.log(results);
});
// Node.js: Event loop = handle 10,000+ concurrent users
Node.js Evolution
Node.js 0.x (2009-2015):
// Callback-based APIs
const fs = require('fs');
fs.readFile('file.txt', 'utf8', function(err, data) {
if (err) throw err;
console.log(data);
});
// Event emitters
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
emitter.on('event', () => {
console.log('Event fired');
});
emitter.emit('event');
Node.js 4.x (2015) - ES6 Support:
// Arrow functions, classes, etc.
const fs = require('fs');
class FileReader {
read(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
}
Node.js 8.x (2017) - async/await:
const fs = require('fs').promises;
async function readFile(path) {
try {
const data = await fs.readFile(path, 'utf8');
return data;
} catch (err) {
console.error(err);
}
}
Node.js 12.x (2019) - ES Modules:
// package.json
{
"type": "module"
}
// Now can use ES modules
import fs from 'fs/promises';
const data = await fs.readFile('file.txt', 'utf8');
Node.js 14.x (2020) - Top-level await:
// No need for async wrapper
import { readFile } from 'fs/promises';
const data = await readFile('config.json', 'utf8');
const config = JSON.parse(data);
export default config;
Node.js 16.x (2021) - Timers Promises:
import { setTimeout } from 'timers/promises';
await setTimeout(1000);
console.log('1 second passed');
// AbortController support
const controller = new AbortController();
setTimeout(1000, 'result', { signal: controller.signal });
controller.abort();
Node.js 18.x (2022) - Fetch API:
// Native fetch (no need for node-fetch)
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Test runner
import { test } from 'node:test';
import assert from 'node:assert';
test('addition', () => {
assert.strictEqual(1 + 1, 2);
});
Node.js 20.x (2023) - Stable Features:
// Permission model
node --experimental-permission --allow-fs-read=/path script.js
// Single executable applications
node --experimental-sea-config sea-config.json
Alternative Runtimes
Deno (2020):
// Secure by default, TypeScript native
// No package.json, no node_modules
// Permissions required
deno run --allow-net --allow-read script.ts
// URL imports
import { serve } from "https://deno.land/std@0.140.0/http/server.ts";
serve((req) => new Response("Hello World"));
// Top-level await
const data = await fetch("https://api.example.com/data");
const json = await data.json();
console.log(json);
// Built-in tools
deno fmt // Format code
deno lint // Lint code
deno test // Run tests
deno bundle // Bundle code
Bun (2022):
// Fast all-in-one runtime
// Built-in bundler, transpiler, package manager
// Fast startup
console.log("Hello from Bun!");
// Native TypeScript
import type { User } from "./types";
// Built-in SQLite
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
// Fast package manager
bun install // Much faster than npm
// Built-in test runner
import { expect, test } from "bun:test";
test("addition", () => {
expect(1 + 1).toBe(2);
});
// Web APIs
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello from Bun!");
}
});
Frameworks & Libraries: Rise and Fall
The jQuery Era (2006-2015)
jQuery - Dominated web development
Peak usage: 2010-2015 (used on 70%+ of websites)
Problem it solved:
- 🎯 Browser incompatibility nightmare (IE6, IE7, IE8, Firefox, Chrome all different)
- 🎯 DOM manipulation was verbose and inconsistent
- 🎯 AJAX was different in every browser
- 🎯 Event handling was inconsistent
- 🎯 CSS selectors weren't supported in old browsers
// Before jQuery - Browser hell
// Different code for different browsers
if (document.getElementById) {
var elem = document.getElementById('myDiv');
} else if (document.all) {
var elem = document.all['myDiv'];
}
// AJAX - Different in IE vs others
var xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
// Event handling - Different everywhere
if (elem.addEventListener) {
elem.addEventListener('click', handler, false);
} else if (elem.attachEvent) {
elem.attachEvent('onclick', handler);
}
// With jQuery - Works everywhere!
$('#myDiv').click(function() {
$.ajax({
url: '/api/data',
success: function(data) {
$('#result').html(data);
}
});
});
Why it succeeded:
- Cross-browser compatibility (one API for all browsers)
- Simple API ($() for everything)
- Plugin ecosystem (10,000+ plugins)
- "Write less, do more"
Why it declined:
- Modern browsers standardized (querySelector, fetch, classList)
- Native APIs improved (no more IE6-8)
- Vanilla JS became easier
- SPAs needed more structure (React, Vue, Angular)
- Performance concerns (manipulating DOM directly)
jQuery plugins ecosystem:
// Thousands of plugins
$('#slider').slick(); // Carousel
$('#datepicker').datepicker(); // Date picker
$('#modal').modal(); // Modals
$('.tooltip').tooltip(); // Tooltips
// jQuery UI
$('#accordion').accordion();
$('#tabs').tabs();
$('#draggable').draggable();
What replaced it:
- Native DOM APIs (querySelector, fetch)
- Modern frameworks (React, Vue, Angular)
- Utility libraries (Lodash)
When it became legacy: ~2017-2018
What replaced it:
- Native DOM APIs (querySelector, fetch, classList)
- Modern frameworks (React, Vue, Angular)
- Utility libraries (Lodash for utilities only)
Pros of jQuery (why it dominated):
- ✅ Cross-browser compatibility (IE6-11)
- ✅ Simple, intuitive API
- ✅ Huge plugin ecosystem (10,000+ plugins)
- ✅ Easy to learn
- ✅ Chainable methods
- ✅ AJAX made simple
- ✅ Animation built-in
Cons of jQuery (why it died):
- ❌ Performance overhead (entire library loaded)
- ❌ DOM manipulation slow for complex UIs
- ❌ No component model
- ❌ No state management
- ❌ Imperative (not declarative)
- ❌ Large bundle size (30KB+ minified)
- ❌ Modern browsers don't need it
- ❌ Not suitable for SPAs
Migration path:
// jQuery → Vanilla JS
$('#id') → document.getElementById('id')
$('.class') → document.querySelectorAll('.class')
$.ajax() → fetch()
$(el).addClass() → el.classList.add()
$(el).on('click', fn) → el.addEventListener('click', fn)
// jQuery → React
$('#app').html(content) → ReactDOM.render(<App />, root)
Current status (2025):
- Still used in legacy projects (~30% of websites)
- WordPress still includes it (but optional)
- Usage declining rapidly (-10% per year)
- Not recommended for new projects
- Maintenance mode only
The MV* Framework Wars (2010-2015)
Backbone.js (2010)
Status: Dead
When it became legacy: ~2015-2016
Peak usage: 2012-2014
What replaced it: React, Vue, Angular
Pros of Backbone:
- ✅ Lightweight (7KB)
- ✅ Flexible, unopinionated
- ✅ RESTful by default
- ✅ Simple event system
- ✅ Works with any template engine
- ✅ Good for small apps
Cons of Backbone:
- ❌ Too minimal (needed many plugins)
- ❌ Boilerplate-heavy
- ❌ Manual DOM updates
- ❌ No two-way binding
- ❌ No component model
- ❌ Required Underscore.js dependency
- ❌ Memory leaks common (zombie views)
- ❌ Routing limited
Migration path:
- Backbone → React (most common)
- Backbone → Vue (easier migration)
- Some stayed on Backbone (legacy apps)
Legacy: Influenced modern frameworks (models, collections, events)
Knockout.js (2010)
Status: Legacy/Maintenance mode
When it became legacy: ~2016-2017
Peak usage: 2011-2014
What replaced it:
- Vue.js (similar reactivity, better DX)
- React
- Angular
Pros of Knockout:
- ✅ MVVM pattern (clean separation)
- ✅ Automatic UI updates (observables)
- ✅ Declarative bindings
- ✅ No dependencies
- ✅ Works with any web framework
- ✅ Good documentation
- ✅ Two-way binding
Cons of Knockout:
- ❌ Observable syntax verbose (
ko.observable()) - ❌ Must call observables as functions
- ❌ No component model (initially)
- ❌ No virtual DOM (slower updates)
- ❌ Limited ecosystem
- ❌ MVVM pattern fell out of favor
- ❌ Computed observables complex
- ❌ Memory management issues
Migration path:
- Knockout → Vue (easiest, similar concepts)
- Knockout → React (more common)
- Some enterprise apps still use it
Current status: Still maintained but not recommended for new projects
AngularJS 1.x (2010-2012)
Status: Dead (officially ended support January 1, 2022)
When it became legacy: ~2016 (when Angular 2 released)
Peak usage: 2013-2016 (used by Google, YouTube, Netflix)
Problem it solved:
- 🎯 jQuery spaghetti code (mixing DOM manipulation with logic)
- 🎯 No structure for large applications
- 🎯 Manual DOM updates when data changed
- 🎯 No two-way data binding (had to sync manually)
- 🎯 Needed complete framework (routing, HTTP, forms, etc.)
The breakthrough:
- ✨ Two-way data binding - Change model, view updates automatically
- ✨ Dependency injection - Testable, modular code
- ✨ Directives - Extend HTML with custom elements
- ✨ Complete framework - Everything included
// Before AngularJS - jQuery spaghetti
var users = [];
$('#add-user').click(function() {
var name = $('#user-name').val();
users.push({ name: name });
// Manually update DOM
$('#user-list').append('<li>' + name + '</li>');
$('#user-count').text(users.length);
$('#user-name').val('');
});
// With AngularJS - Two-way binding
angular.module('app', [])
.controller('MainCtrl', function($scope) {
$scope.users = [];
$scope.addUser = function() {
$scope.users.push({ name: $scope.userName });
$scope.userName = '';
// View updates automatically!
};
});
<!-- AngularJS template - Declarative -->
<div ng-controller="MainCtrl">
<input ng-model="userName">
<button ng-click="addUser()">Add</button>
<ul>
<li ng-repeat="user in users">{{user.name}}</li>
</ul>
<p>Total: {{users.length}}</p>
</div>
// AngularJS 1.x - Full example
angular.module('app', [])
.controller('MainCtrl', function($scope, $http) {
$scope.users = [];
$http.get('/api/users').then(function(response) {
$scope.users = response.data;
});
$scope.addUser = function(user) {
$scope.users.push(user);
};
});
What replaced it:
- Angular 2+ (complete rewrite, difficult migration)
- React (most common migration)
- Vue (easier migration)
Pros of AngularJS:
- ✅ Two-way data binding (revolutionary at the time)
- ✅ Dependency injection
- ✅ Directives (reusable components)
- ✅ Complete framework (routing, HTTP, forms)
- ✅ Google backing
- ✅ Large community
- ✅ Good for enterprise apps
- ✅ Testable (built-in testing support)
Cons of AngularJS:
- ❌ Digest cycle performance issues (slow with >2000 watchers)
- ❌ Two-way binding caused unpredictable updates
- ❌ Steep learning curve (directives, $scope, digest)
- ❌ $scope confusion (inheritance issues)
- ❌ Dirty checking inefficient
- ❌ No component model (initially)
- ❌ Directive API complex
- ❌ Version 2 was complete rewrite (no upgrade path)
- ❌ Too much "magic"
- ❌ Large bundle size
Migration path:
- AngularJS → Angular 2+ (very difficult, complete rewrite)
- AngularJS → React (most common, easier)
- AngularJS → Vue (easiest migration)
- Many apps stayed on AngularJS until 2022
Why the migration was painful:
- Zero compatibility between AngularJS and Angular 2+
- Different language (TypeScript required)
- Different concepts (no $scope, no digest)
- Had to rewrite entire app
Current status:
- Dead (no security updates after 2022)
- Legacy apps being migrated
- Not recommended even for maintenance
Ember.js (2011)
Status: Alive but niche (declining)
When it started declining: ~2015-2016
Peak usage: 2013-2016
What's replacing it:
- React (most migrations)
- Vue
- Modern frameworks
Pros of Ember:
- ✅ Convention over configuration
- ✅ Complete framework (batteries included)
- ✅ Ember CLI (best CLI at the time)
- ✅ Strong opinions (consistency)
- ✅ Ember Data (powerful ORM)
- ✅ Backward compatibility (smooth upgrades)
- ✅ Great for large teams
- ✅ Stable releases
- ✅ Good documentation
- ✅ Addons ecosystem
Cons of Ember:
- ❌ Too opinionated (hard to deviate)
- ❌ Steep learning curve
- ❌ Large bundle size (200KB+)
- ❌ Slower than React/Vue
- ❌ Smaller ecosystem than React
- ❌ Less flexible
- ❌ Harder to hire developers
- ❌ "The Ember Way" or nothing
- ❌ Verbose syntax
- ❌ Slower innovation
Current usage (2025):
- Some large legacy apps (LinkedIn, Apple Music, Discourse)
- Convention over configuration still valued
- Stable but shrinking community
- Hard to justify for new projects
- Good for teams that value stability over innovation
Migration path:
- Ember → React (most common)
- Ember → Vue
- Many apps staying on Ember (stable, works well)
Current status:
- Still maintained and updated
- Octane edition (2019) modernized it
- But market share declining
- Not recommended for new projects unless specific needs
// Ember.js - still used
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TodoComponent extends Component {
@tracked todos = [];
@action
addTodo(text) {
this.todos = [...this.todos, { text, done: false }];
}
}
The Modern Framework Era (2013-Present)
React (2013-Present)
Status: Dominant
Problem it solved:
- 🎯 jQuery/Backbone couldn't handle complex UIs (Facebook news feed)
- 🎯 DOM manipulation was slow and hard to reason about
- 🎯 Two-way data binding (Angular 1) caused performance issues
- 🎯 Hard to know what changed when state updated
- 🎯 Needed declarative UI (describe what, not how)
The breakthrough:
- ✨ Virtual DOM - Diff and update only what changed
- ✨ One-way data flow - Predictable state management
- ✨ Components - Reusable, composable UI pieces
- ✨ JSX - Write HTML in JavaScript (controversial but powerful)
// Before React - jQuery spaghetti
function updateUI(data) {
$('#user-name').text(data.name);
$('#user-email').text(data.email);
if (data.premium) {
$('#premium-badge').show();
} else {
$('#premium-badge').hide();
}
// Manually track what changed, update DOM
// Gets messy fast!
}
// With React - Declarative
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
{user.premium && <span className="badge">Premium</span>}
</div>
);
// React figures out what changed and updates DOM
}
Evolution:
// 2013: Class components
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return <h1>Count: {this.state.count}</h1>;
}
}
// 2015: ES6 classes
class Welcome extends React.Component {
state = { count: 0 };
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.increment}>+</button>
</div>
);
}
}
// 2019: Hooks (game changer)
function Welcome() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// 2020: Custom hooks
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset };
}
function Counter() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<h1>{count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// 2024: Server Components (Next.js 13+)
// app/page.tsx
async function Page() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return <div>{json.title}</div>;
}
// Runs on server, no JavaScript sent to client
React Ecosystem:
// State management evolution
// 2015: Redux
import { createStore } from 'redux';
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
};
const store = createStore(reducer);
// 2018: Redux Toolkit (simplified)
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: state => { state.count += 1; }
}
});
const store = configureStore({
reducer: { counter: counterSlice.reducer }
});
// 2019: Context + Hooks (built-in)
const CountContext = React.createContext();
function CountProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
{children}
</CountContext.Provider>
);
}
function useCount() {
return useContext(CountContext);
}
// 2020: Zustand (simple)
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
// 2021: Jotai (atomic)
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// 2022: TanStack Query (server state)
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json())
});
if (isLoading) return <div>Loading...</div>;
return <div>{data.map(user => <div key={user.id}>{user.name}</div>)}</div>;
}
Vue.js (2014-Present)
Status: Very popular, especially in Asia
Problem it solved:
- 🎯 React's JSX was controversial (mixing HTML in JavaScript)
- 🎯 Angular 2 was too complex (TypeScript required, steep learning curve)
- 🎯 Needed something easier than Angular, more approachable than React
- 🎯 Wanted templates (like Angular) but simpler
- 🎯 Progressive framework (use as little or as much as needed)
The breakthrough:
- ✨ Approachable - Easiest learning curve of the big 3
- ✨ Templates - HTML-based (familiar to designers)
- ✨ Reactive - Like React but simpler
- ✨ Progressive - Can use just for one component or entire app
- ✨ Single File Components - HTML, CSS, JS in one file
// React - JSX (controversial)
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Angular - Complex
@Component({
selector: 'app-counter',
template: '<button (click)="increment()">{{count}}</button>'
})
export class CounterComponent {
count = 0;
increment() { this.count++; }
}
// Vue - Simple, familiar templates
<template>
<button @click="count++">{{ count }}</button>
</template>
<script>
export default {
data() {
return { count: 0 };
}
};
</script>
Evolution:
// 2014: Vue 1.x - Options API
new Vue({
el: '#app',
data: {
count: 0
},
methods: {
increment() {
this.count++;
}
}
});
// 2016: Vue 2.x - Improved
Vue.component('counter', {
data() {
return { count: 0 };
},
template: `
<div>
<h1>{{ count }}</h1>
<button @click="increment">+</button>
</div>
`,
methods: {
increment() {
this.count++;
}
}
});
// 2020: Vue 3.x - Composition API
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
return { count, doubled, increment };
}
};
// 2023: Vue 3.3 - Script setup (cleaner)
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
</script>
<template>
<div>
<h1>{{ count }}</h1>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">+</button>
</div>
</template>
Vue Ecosystem:
- Vuex (2015) → Pinia (2021) - State management
- Vue Router - Official routing
- Nuxt.js - Full-stack framework (like Next.js for React)
Angular 2+ (2016-Present)
Status: Popular in enterprise
Complete rewrite from AngularJS:
// Angular 2+ (2016)
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<h1>{{ count }}</h1>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}
// Angular 14+ (2022) - Standalone components
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<div>
<h1>{{ count }}</h1>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}
// Angular 16+ (2023) - Signals
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<h1>{{ count() }}</h1>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(c => c + 1);
}
}
Pros of Angular 2+:
- ✅ TypeScript from the start (type safety)
- ✅ Complete framework (routing, HTTP, forms, testing)
- ✅ Dependency injection (enterprise-grade)
- ✅ RxJS integration (powerful async)
- ✅ Angular CLI (excellent tooling)
- ✅ Opinionated (consistency in large teams)
- ✅ Google backing (long-term support)
- ✅ Great for enterprise
- ✅ Strong conventions
- ✅ Ahead-of-Time compilation
- ✅ Built-in form validation
- ✅ Internationalization built-in
Cons of Angular 2+:
- ❌ Steep learning curve (RxJS, TypeScript, DI)
- ❌ Verbose (lots of boilerplate)
- ❌ Large bundle size (100KB+ minimum)
- ❌ Slower than React/Vue
- ❌ Breaking changes between versions (initially)
- ❌ RxJS required (complex for beginners)
- ❌ Decorator-heavy (lots of magic)
- ❌ Zone.js overhead
- ❌ Less flexible than React
- ❌ Smaller ecosystem than React
Why Angular survived:
- TypeScript from the start (ahead of its time)
- Enterprise adoption (banks, insurance, government)
- Complete framework (no decision fatigue)
- Google backing (trust and stability)
- Strong CLI and tooling
- Good for large teams
- Backward compatibility improved
When to use Angular (2025):
- ✅ Enterprise applications
- ✅ Large teams (need consistency)
- ✅ TypeScript-first projects
- ✅ Need complete framework
- ✅ Long-term projects (stability)
When NOT to use Angular:
- ❌ Small projects (too heavy)
- ❌ Need fast learning curve
- ❌ Performance critical (use React/Svelte)
- ❌ Small bundle size needed
- ❌ Startup/fast iteration
Current status (2025):
- Alive and well (15% market share)
- Angular 17+ with Signals (modern reactivity)
- Standalone components (less boilerplate)
- Still best for enterprise
- Not growing but stable
New Generation Frameworks (2019-Present)
Svelte (2016, popular 2019+)
Status: Growing rapidly
Philosophy: Compile-time framework (no virtual DOM)
<!-- Before: React -->
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
<!-- After: Svelte -->
<script>
let count = 0;
function increment() {
count += 1; // Just mutate!
}
</script>
<div>
<h1>{count}</h1>
<button on:click={increment}>+</button>
</div>
<!-- Reactive declarations -->
<script>
let count = 0;
$: doubled = count * 2; // Automatically updates
$: if (count > 10) {
alert('Count is high!');
}
</script>
<!-- SvelteKit (2021) - Full-stack framework -->
<script>
export let data; // From load function
</script>
<h1>{data.title}</h1>
Pros of Svelte:
- ✅ Less boilerplate (cleanest syntax)
- ✅ No virtual DOM (compiles to vanilla JS)
- ✅ Smallest bundle sizes (10-20KB typical)
- ✅ Easier to learn (feels like HTML/CSS/JS)
- ✅ Great developer experience
- ✅ Reactive by default ($: syntax)
- ✅ Built-in animations/transitions
- ✅ Scoped CSS by default
- ✅ Fast performance
- ✅ No runtime overhead
- ✅ True reactivity (not virtual DOM diffing)
Cons of Svelte:
- ❌ Smaller ecosystem than React
- ❌ Fewer jobs (but growing)
- ❌ Less mature (newer framework)
- ❌ Fewer third-party components
- ❌ Compiler adds build step
- ❌ Some edge cases with reactivity
- ❌ Smaller community
- ❌ Less Stack Overflow answers
- ❌ Corporate backing unclear (Rich Harris at Vercel)
When to use Svelte (2025):
- ✅ New projects (greenfield)
- ✅ Performance critical apps
- ✅ Small bundle size needed
- ✅ Developer experience priority
- ✅ Content-heavy sites
- ✅ Interactive visualizations
Current status (2025):
- Growing rapidly (10% market share)
- SvelteKit stable and mature
- Svelte 5 with Runes (new reactivity)
- Recommended for new projects
- Great for indie developers
Solid.js (2021)
Status: Rising star (niche but growing)
Philosophy: Fine-grained reactivity (like Svelte but JSX)
Peak performance: Fastest framework in benchmarks
// Solid.js
import { createSignal, createEffect } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log('Count is:', count());
});
return (
<div>
<h1>{count()}</h1>
<button onClick={() => setCount(count() + 1)}>+</button>
</div>
);
}
// No virtual DOM, but JSX
// Extremely fast
// React-like API but different mental model
Pros of Solid.js:
- ✅ Fastest framework (beats all in benchmarks)
- ✅ Fine-grained reactivity (no virtual DOM)
- ✅ JSX (familiar to React developers)
- ✅ Small bundle size (7KB)
- ✅ No re-renders (only updates what changed)
- ✅ React-like API (easier migration)
- ✅ TypeScript support
- ✅ Great performance
- ✅ Signals-based (modern reactivity)
Cons of Solid.js:
- ❌ Very small ecosystem
- ❌ Few jobs
- ❌ Different mental model (not React)
- ❌ Signals can be confusing
- ❌ Less mature
- ❌ Fewer components/libraries
- ❌ Small community
- ❌ JSX looks like React but behaves differently
- ❌ Learning curve (reactivity model)
When to use Solid.js (2025):
- ✅ Performance is critical
- ✅ Coming from React (familiar JSX)
- ✅ Want modern reactivity
- ✅ Small bundle size needed
- ✅ Experimental/side projects
Current status (2025):
- Niche but growing
- SolidStart (meta-framework) maturing
- Used by some performance-critical apps
- Not recommended for most production apps yet
- Great for learning modern reactivity
Qwik (2023)
Status: Experimental but promising
Philosophy: Resumability (instant loading, no hydration)
Creator: Miško Hevery (creator of AngularJS)
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<div>
<h1>{count.value}</h1>
<button onClick$={() => count.value++}>+</button>
</div>
);
});
// Loads only what's needed
// No hydration, just resumability
// Instant interactive
Pros of Qwik:
- ✅ Instant loading (resumability)
- ✅ No hydration (faster than SSR)
- ✅ Lazy loads everything automatically
- ✅ Best Time to Interactive (TTI)
- ✅ JSX (familiar)
- ✅ Signals-based reactivity
- ✅ Great for content-heavy sites
- ✅ Innovative approach
Cons of Qwik:
- ❌ Very new (experimental)
- ❌ Tiny ecosystem
- ❌ Almost no jobs
- ❌ Different mental model ($ syntax)
- ❌ Not proven at scale
- ❌ Small community
- ❌ Optimizer can be confusing
- ❌ Build complexity
- ❌ Not recommended for production yet
When to use Qwik (2025):
- ✅ Experimental projects
- ✅ Content-heavy sites (blogs, docs)
- ✅ Performance is critical
- ✅ Want to try cutting-edge tech
- ❌ NOT for production apps yet
Current status (2025):
- Experimental
- QwikCity (meta-framework) available
- Interesting concept but unproven
- Wait for more maturity
Meta-Frameworks (Full-Stack)
Next.js (React) - 2016-Present
Status: Industry standard for React
Evolution:
// 2016: Next.js 1.0 - Simple SSR
// pages/index.js
export default function Home() {
return <h1>Hello Next.js</h1>;
}
// 2018: Next.js 7 - Static export
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data');
return { props: { data } };
}
// 2020: Next.js 9.3 - SSG + ISR
export async function getStaticProps() {
return {
props: { data },
revalidate: 60 // Regenerate every 60 seconds
};
}
// 2022: Next.js 13 - App Router (Server Components)
// app/page.tsx
async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data.title}</div>;
}
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
// 2023: Next.js 13.4 - Stable App Router
// Server Actions
async function createUser(formData: FormData) {
'use server';
const name = formData.get('name');
await db.users.create({ name });
}
// Client component
'use client';
export default function Form() {
return (
<form action={createUser}>
<input name="name" />
<button>Submit</button>
</form>
);
}
Nuxt.js (Vue) - 2016-Present
Status: Standard for Vue
// Nuxt 2 (2018)
export default {
async asyncData({ $axios }) {
const data = await $axios.$get('/api/data');
return { data };
}
};
// Nuxt 3 (2022) - Composition API
<script setup>
const { data } = await useFetch('/api/data');
</script>
<template>
<div>{{ data }}</div>
</template>
Remix (React) - 2021
Status: Growing, acquired by Shopify
// Loader (server-side)
export async function loader({ params }) {
const user = await getUser(params.id);
return json({ user });
}
// Action (server-side)
export async function action({ request }) {
const formData = await request.formData();
await createUser(formData);
return redirect('/users');
}
// Component
export default function User() {
const { user } = useLoaderData();
return (
<div>
<h1>{user.name}</h1>
<Form method="post">
<input name="name" />
<button>Update</button>
</Form>
</div>
);
}
Astro (2021)
Status: Popular for content sites
Philosophy: Ship zero JavaScript by default
---
// Server-side code
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
---
<html>
<body>
<h1>Blog Posts</h1>
{posts.map(post => (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</body>
</html>
<!-- Can use any framework -->
---
import ReactCounter from './ReactCounter.jsx';
import VueCounter from './VueCounter.vue';
import SvelteCounter from './SvelteCounter.svelte';
---
<ReactCounter client:load />
<VueCounter client:visible />
<SvelteCounter client:idle />
Utility Libraries
Lodash (2012-Present)
Status: Still widely used
// Before Lodash
var users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
];
var names = users.map(function(u) { return u.name; });
var sorted = users.sort(function(a, b) { return a.age - b.age; });
// With Lodash
import _ from 'lodash';
const names = _.map(users, 'name');
const sorted = _.sortBy(users, 'age');
const grouped = _.groupBy(users, 'age');
const debounced = _.debounce(handleInput, 300);
// Deep clone
const clone = _.cloneDeep(obj);
// Get nested property safely
const value = _.get(obj, 'a.b.c.d', 'default');
Why still used:
- Comprehensive utilities
- Battle-tested
- Tree-shakeable (lodash-es)
- Some functions still not in native JS
Alternatives:
- Native JS (for many use cases)
- Ramda (functional programming)
- Just (smaller, modular)
Moment.js → Day.js / date-fns
Moment.js (2011-2020)
Status: Deprecated (maintenance mode)
// Moment.js
moment().format('YYYY-MM-DD');
moment().add(7, 'days');
moment().subtract(1, 'month');
Why it died:
- Mutable API (confusing)
- Large bundle size
- Not tree-shakeable
Replaced by:
// Day.js (2018) - Moment.js compatible API
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD');
dayjs().add(7, 'day');
// date-fns (2016) - Functional, tree-shakeable
import { format, addDays, subMonths } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
addDays(new Date(), 7);
// Native Temporal (Stage 3, coming soon)
Temporal.Now.plainDateISO().toString(); // '2025-12-04'
Temporal.Now.plainDateISO().add({ days: 7 });
Testing Ecosystem Evolution
Test Runners
QUnit (2008) - jQuery's test framework
Status: Dead for new projects
When it became legacy: ~2014-2015
What replaced it: Jasmine, Mocha, Jest
QUnit.test('addition', function(assert) {
assert.equal(1 + 1, 2);
});
Pros of QUnit:
- ✅ Simple API
- ✅ No dependencies
- ✅ Browser-based testing
- ✅ Good for jQuery projects
Cons of QUnit:
- ❌ Limited features
- ❌ No mocking built-in
- ❌ Tied to jQuery ecosystem
- ❌ No async/await support (initially)
- ❌ Basic assertions only
- ❌ No code coverage
Current status: Only for legacy jQuery projects
Jasmine (2010)
Status: Legacy/Declining
When it became legacy: ~2016-2017
Peak usage: 2011-2015
What replaced it: Jest, Mocha
describe('Calculator', function() {
it('should add numbers', function() {
expect(add(1, 2)).toBe(3);
});
});
Pros of Jasmine:
- ✅ BDD syntax (describe/it)
- ✅ No dependencies
- ✅ Built-in assertions
- ✅ Built-in spies/mocks
- ✅ Browser and Node.js support
- ✅ Good documentation
Cons of Jasmine:
- ❌ Slow test execution
- ❌ No snapshot testing
- ❌ No code coverage built-in
- ❌ No watch mode
- ❌ Configuration complex
- ❌ Async testing awkward
- ❌ No parallel execution
- ❌ Outdated compared to Jest
Migration path: Jasmine → Jest (easy, similar API)
Current status: Still used in Angular projects (default), but Jest preferred
Mocha (2011) + Chai
Status: Still used but declining
When it started declining: ~2017-2018
Peak usage: 2013-2017
What's replacing it: Jest, Vitest
const { expect } = require('chai');
describe('Calculator', () => {
it('should add numbers', () => {
expect(add(1, 2)).to.equal(3);
});
});
Pros of Mocha:
- ✅ Flexible (bring your own assertion library)
- ✅ Good async support
- ✅ Extensible (many plugins)
- ✅ Works in browser and Node.js
- ✅ Mature and stable
- ✅ Good documentation
Cons of Mocha:
- ❌ Requires separate assertion library (Chai)
- ❌ Requires separate mocking library (Sinon)
- ❌ No snapshot testing
- ❌ No code coverage built-in
- ❌ Configuration needed
- ❌ Slower than Jest
- ❌ No watch mode built-in
- ❌ More setup required
Migration path: Mocha → Jest or Vitest
Current status (2025):
- Still maintained
- Used in some legacy projects
- Not recommended for new projects
- Jest/Vitest better alternatives
Jest (2014, Facebook)
Status: Dominant for React
Problem it solved:
- 🎯 Mocha/Jasmine required too much setup (test runner + assertion + mocking + coverage)
- 🎯 No snapshot testing (hard to test React components)
- 🎯 Slow test runs (no parallelization)
- 🎯 Mocking was painful (Sinon.js separate library)
- 🎯 No watch mode (had to rerun manually)
The breakthrough:
- ✨ Zero config - Works out of the box
- ✨ All-in-one - Test runner + assertions + mocking + coverage
- ✨ Snapshot testing - Perfect for React components
- ✨ Fast - Parallel execution, smart watch mode
- ✨ Great DX - Interactive, clear error messages
// Before Jest - Mocha + Chai + Sinon + Istanbul
// package.json
"devDependencies": {
"mocha": "^10.0.0",
"chai": "^4.3.0",
"sinon": "^15.0.0",
"istanbul": "^0.4.5"
}
// test.js
const { expect } = require('chai');
const sinon = require('sinon');
describe('Calculator', () => {
it('should add', () => {
expect(add(1, 2)).to.equal(3);
});
it('should mock', () => {
const mock = sinon.stub();
mock.returns(42);
expect(mock()).to.equal(42);
});
});
// Run tests
mocha test.js
// Run coverage
istanbul cover _mocha
// With Jest - Zero config, all-in-one
// package.json
"devDependencies": {
"jest": "^29.0.0"
}
// test.js
describe('Calculator', () => {
test('adds numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('mocks functions', () => {
const mock = jest.fn().mockReturnValue(42);
expect(mock()).toBe(42);
});
});
// Run tests with coverage
jest --coverage
// Jest - batteries included
describe('Calculator', () => {
test('adds numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('mocks functions', () => {
const mock = jest.fn();
mock('hello');
expect(mock).toHaveBeenCalledWith('hello');
});
});
// Snapshot testing
test('renders correctly', () => {
const tree = renderer.create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
// Async testing
test('fetches data', async () => {
const data = await fetchData();
expect(data).toEqual({ name: 'John' });
});
Pros of Jest:
- ✅ Zero config (works out of the box)
- ✅ Fast (parallel execution)
- ✅ Built-in mocking (jest.fn(), jest.mock())
- ✅ Snapshot testing (unique feature)
- ✅ Great error messages
- ✅ Watch mode (interactive)
- ✅ Code coverage built-in
- ✅ Works with React (React Testing Library)
- ✅ Large community
- ✅ Facebook backing
- ✅ Great documentation
- ✅ Timer mocking (jest.useFakeTimers())
Cons of Jest:
- ❌ Slow startup (especially large projects)
- ❌ ESM support problematic (until recently)
- ❌ Heavy (large dependency)
- ❌ Can be slow on large codebases
- ❌ Snapshot tests can be overused
- ❌ Mocking can be confusing
- ❌ Global namespace pollution
- ❌ Configuration can get complex
When to use Jest (2025):
- ✅ React projects (standard)
- ✅ Need snapshot testing
- ✅ Want zero config
- ✅ Large community support needed
What's replacing Jest: Vitest (for Vite projects)
Current status (2025):
- Still dominant (40% market share)
- Standard for React
- Being challenged by Vitest
- Still recommended for most projects
Vitest (2021)
Status: Rising fast (replacing Jest in Vite projects)
Created by: Vite team (Anthony Fu)
Problem it solved:
- 🎯 Jest was slow with Vite (different module systems)
- 🎯 Jest's ESM support was problematic
- 🎯 Vite config and Jest config were separate (duplication)
- 🎯 Jest startup was slow (especially on large projects)
- 🎯 Wanted same speed as Vite dev server for tests
The breakthrough:
- ✨ Uses Vite - Same config, same transforms, same plugins
- ✨ Extremely fast - Instant startup, fast execution
- ✨ Jest-compatible API - Easy migration from Jest
- ✨ Native ESM - No transformation needed
- ✨ HMR for tests - Tests rerun instantly on change
// Jest with Vite - Slow, separate config
// vite.config.js
export default {
plugins: [react()],
// Vite config
};
// jest.config.js
export default {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
// Duplicate config!
};
// Slow startup (5-10 seconds)
npm run test
// Vitest - Fast, shared config
// vite.config.js
export default {
plugins: [react()],
test: {
// Test config in same file!
}
};
// Instant startup (200ms)
npm run test
import { describe, test, expect } from 'vitest';
describe('Calculator', () => {
test('adds numbers', () => {
expect(add(1, 2)).toBe(3);
});
});
Pros of Vitest:
- ✅ Extremely fast (uses Vite)
- ✅ Jest-compatible API (easy migration)
- ✅ Native ESM support
- ✅ Vite config reuse (no separate config)
- ✅ Fast HMR for tests
- ✅ TypeScript support out of the box
- ✅ Component testing built-in
- ✅ Watch mode (instant)
- ✅ Snapshot testing
- ✅ Code coverage (c8/istanbul)
- ✅ Multi-threading
- ✅ Better error messages than Jest
Cons of Vitest:
- ❌ Newer (less mature than Jest)
- ❌ Smaller community
- ❌ Requires Vite (or Vite config)
- ❌ Some Jest features missing
- ❌ Less Stack Overflow answers
- ❌ Fewer plugins
When to use Vitest (2025):
- ✅ Using Vite (obvious choice)
- ✅ New projects
- ✅ Want speed
- ✅ TypeScript projects
- ✅ Vue/Svelte/Solid projects
Migration from Jest:
- Very easy (API compatible)
- Change imports
- Update config
- Most tests work as-is
Current status (2025):
- Rapidly growing (20% market share)
- Standard for Vite projects
- Recommended for new projects
- Will likely overtake Jest
Bun Test (2023)
Status: New, very fast, experimental
import { test, expect } from 'bun:test';
test('addition', () => {
expect(1 + 1).toBe(2);
});
Pros of Bun Test:
- ✅ Extremely fast (fastest test runner)
- ✅ Built into Bun (no install)
- ✅ Jest-compatible API
- ✅ TypeScript native
- ✅ Snapshot testing
- ✅ Watch mode
- ✅ Mocking built-in
- ✅ Zero config
Cons of Bun Test:
- ❌ Very new (immature)
- ❌ Requires Bun runtime
- ❌ Small ecosystem
- ❌ Missing some Jest features
- ❌ Not production-ready yet
- ❌ Limited documentation
- ❌ Bugs and edge cases
When to use Bun Test (2025):
- ✅ Already using Bun
- ✅ Experimental projects
- ✅ Want maximum speed
- ❌ NOT for production yet
Current status (2025):
- Experimental
- Very promising
- Wait for more maturity
- Good for side projects
End-to-End Testing
Selenium (2004)
Status: Legacy but still used (especially enterprise)
When it became legacy: ~2017-2018
Peak usage: 2008-2016
What's replacing it: Cypress, Playwright
const { Builder, By } = require('selenium-webdriver');
const driver = await new Builder().forBrowser('chrome').build();
await driver.get('http://localhost:3000');
await driver.findElement(By.id('username')).sendKeys('john');
await driver.findElement(By.id('submit')).click();
Pros of Selenium:
- ✅ Multi-browser support (all browsers)
- ✅ Multi-language (Java, Python, C#, JS)
- ✅ Mature and stable
- ✅ Large community
- ✅ Works with mobile (Appium)
- ✅ Grid support (parallel testing)
- ✅ Industry standard (for years)
Cons of Selenium:
- ❌ Slow and flaky
- ❌ Complex setup
- ❌ No automatic waiting (manual waits needed)
- ❌ Poor error messages
- ❌ Verbose API
- ❌ No time-travel debugging
- ❌ No automatic screenshots/videos
- ❌ WebDriver protocol overhead
- ❌ Hard to debug
- ❌ Outdated compared to modern tools
When to use Selenium (2025):
- ✅ Legacy projects already using it
- ✅ Need Java/Python/C# (not JavaScript)
- ✅ Enterprise with existing Selenium Grid
- ❌ NOT recommended for new JavaScript projects
Current status (2025):
- Still maintained
- Used in enterprise (legacy)
- Being replaced by Playwright/Cypress
- Not recommended for new projects
Protractor (2013, Angular)
Status: Dead (officially deprecated 2021, end of life 2022)
When it became legacy: 2020-2021
What replaced it:
- Cypress (recommended by Angular team)
- Playwright
- WebdriverIO
Pros of Protractor:
- ✅ Built for Angular (automatic waiting)
- ✅ Angular-specific locators
- ✅ Good for Angular apps
Cons of Protractor:
- ❌ Only good for Angular
- ❌ Built on Selenium (slow)
- ❌ Flaky tests
- ❌ Complex configuration
- ❌ Deprecated (no future)
Migration path:
- Protractor → Cypress (Angular team recommendation)
- Protractor → Playwright
Current status: Dead, migrate immediately
Cypress (2017)
Status: Very popular
describe('Login', () => {
it('should login successfully', () => {
cy.visit('/login');
cy.get('#username').type('john');
cy.get('#password').type('secret');
cy.get('#submit').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome, John');
});
});
// Time travel debugging
// Automatic waiting
// Great DX
// Screenshots/videos
Pros of Cypress:
- ✅ Amazing developer experience
- ✅ Time-travel debugging (see every step)
- ✅ Automatic waiting (no manual waits)
- ✅ Real-time reloading
- ✅ Screenshots/videos automatic
- ✅ Great documentation
- ✅ Easy to learn
- ✅ Network stubbing built-in
- ✅ Great error messages
- ✅ Interactive test runner
- ✅ Large community
- ✅ Component testing
Cons of Cypress:
- ❌ Runs in browser (architecture limitation)
- ❌ No multi-tab support
- ❌ No native events (synthetic events)
- ❌ Slower than Playwright
- ❌ Chrome/Edge/Firefox only (no Safari)
- ❌ Can't test multiple domains easily
- ❌ Parallel execution requires paid plan
- ❌ Some limitations due to browser-based architecture
When to use Cypress (2025):
- ✅ Developer experience priority
- ✅ Need time-travel debugging
- ✅ Single-page applications
- ✅ Don't need Safari testing
- ✅ Want easy learning curve
Current status (2025):
- Still very popular (30% market share)
- Great for SPAs
- Being challenged by Playwright
- Still recommended for many projects
Playwright (2020, Microsoft)
Status: Growing rapidly
import { test, expect } from '@playwright/test';
test('login', async ({ page }) => {
await page.goto('/login');
await page.fill('#username', 'john');
await page.fill('#password', 'secret');
await page.click('#submit');
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('text=Welcome, John')).toBeVisible();
});
// Multi-browser (Chrome, Firefox, Safari, Edge)
// Parallel execution
// Auto-wait
// Network interception
// Mobile emulation
Pros of Playwright:
- ✅ Multi-browser (Chrome, Firefox, Safari, Edge)
- ✅ Faster than Cypress
- ✅ Parallel execution built-in
- ✅ Auto-wait (smart waiting)
- ✅ Network interception
- ✅ Mobile emulation
- ✅ Multiple tabs/contexts
- ✅ Native events (real browser events)
- ✅ Screenshots/videos/traces
- ✅ Great for CI/CD
- ✅ Microsoft backing
- ✅ Codegen (record tests)
- ✅ Component testing
- ✅ API testing built-in
- ✅ Better architecture than Cypress
Cons of Playwright:
- ❌ No time-travel debugging (like Cypress)
- ❌ Steeper learning curve
- ❌ Less interactive test runner
- ❌ Smaller community than Cypress
- ❌ Newer (less mature)
- ❌ Fewer resources/tutorials
When to use Playwright (2025):
- ✅ Need Safari testing
- ✅ Need multi-tab support
- ✅ Performance critical
- ✅ CI/CD focused
- ✅ Need parallel execution
- ✅ Enterprise projects
- ✅ New projects (recommended)
Why Playwright is winning:
- Faster than Cypress (2-3x)
- Better browser support (Safari!)
- More powerful API
- Better for CI/CD
- Microsoft backing (long-term support)
- Better architecture (no browser limitations)
Migration from Cypress:
- API similar but different
- Rewrite tests (no automatic migration)
- Worth it for better features
Current status (2025):
- Rapidly growing (40% market share)
- Industry standard for new projects
- Recommended over Cypress for most cases
- Will likely become dominant
Puppeteer (2017, Google)
Status: Still used for automation
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
await browser.close();
// Headless Chrome automation
// PDF generation
// Scraping
// Not primarily for testing
Pros of Puppeteer:
- ✅ Chrome DevTools Protocol (powerful)
- ✅ Great for automation (scraping, PDFs)
- ✅ Headless Chrome control
- ✅ Google backing
- ✅ Good documentation
- ✅ Fast
Cons of Puppeteer:
- ❌ Chrome/Chromium only
- ❌ Not designed for testing (use Playwright)
- ❌ No multi-browser
- ❌ Less testing features
When to use Puppeteer (2025):
- ✅ Web scraping
- ✅ PDF generation
- ✅ Screenshots
- ✅ Browser automation (non-testing)
- ❌ NOT for E2E testing (use Playwright)
Current status (2025):
- Still maintained
- Good for automation
- Playwright better for testing
- Use for scraping/PDFs, not testing
Build Tools & Module Systems
Module Bundlers Evolution
RequireJS (2010) - AMD
Status: Dead
// AMD (Asynchronous Module Definition)
define(['jquery', 'lodash'], function($, _) {
return {
doSomething: function() {
// ...
}
};
});
Browserify (2011)
Status: Dead
When it became legacy: ~2015-2016
Peak usage: 2012-2015
What replaced it: Webpack, Rollup
Problem it solved:
- 🎯 Node.js had
require()for modules, browsers didn't - 🎯 Wanted to use npm packages in the browser
- 🎯 No standard module system for browsers (before ES6 modules)
- 🎯 Had to manually include
<script>tags in order
// Before Browserify - Manual script tags in HTML
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="app.js"></script>
// Order matters! Dependencies must load first
// With Browserify - Use Node.js style require()
const $ = require('jquery');
const _ = require('lodash');
// Build
browserify app.js -o bundle.js
// Creates single bundle with all dependencies
Pros of Browserify:
- ✅ First to bring Node.js modules to browser
- ✅ Simple concept
- ✅ npm packages in browser
- ✅ Unix philosophy (do one thing)
- ✅ Streaming
Cons of Browserify:
- ❌ No code splitting
- ❌ No hot module replacement
- ❌ Slower than Webpack
- ❌ Limited features
- ❌ No tree shaking
- ❌ No asset handling (CSS, images)
- ❌ Webpack more powerful
Current status: Dead, use Webpack/Vite/Rollup
Webpack (2012-Present)
Status: Still dominant but challenged
Problem it solved:
- 🎯 Browserify only handled JavaScript, not CSS/images/fonts
- 🎯 No code splitting (one giant bundle = slow loading)
- 🎯 No Hot Module Replacement (had to refresh entire page)
- 🎯 Complex apps needed asset pipeline (CSS, images, fonts, etc.)
- 🎯 Needed build optimizations (minification, tree shaking)
Evolution:
// Webpack 1 (2014)
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.js$/, loader: 'babel-loader' }
]
}
};
// Webpack 2 (2017) - Tree shaking
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' }
]
}
};
// Webpack 4 (2018) - Zero config
// No config needed for simple cases
module.exports = {
mode: 'production', // or 'development'
entry: './src/index.js'
};
// Webpack 5 (2020) - Module Federation
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js'
}
})
]
};
// Code splitting
import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
// Use lodash
});
Pros of Webpack:
- ✅ Powerful plugin system (thousands of plugins)
- ✅ Code splitting (dynamic imports)
- ✅ Hot Module Replacement (HMR)
- ✅ Handles everything (CSS, images, fonts, etc.)
- ✅ Large ecosystem
- ✅ Tree shaking
- ✅ Production optimizations
- ✅ Module Federation (micro-frontends)
- ✅ Mature and battle-tested
- ✅ Works with any framework
- ✅ Great for complex projects
Cons of Webpack:
- ❌ Slow builds (especially large projects)
- ❌ Complex configuration (webpack.config.js hell)
- ❌ Steep learning curve
- ❌ Slow HMR (compared to Vite)
- ❌ Large config files
- ❌ Hard to debug
- ❌ Newer tools much faster
When to use Webpack (2025):
- ✅ Legacy projects already using it
- ✅ Need Module Federation
- ✅ Complex build requirements
- ✅ Enterprise projects (proven at scale)
- ❌ NOT recommended for new projects (use Vite)
What's replacing Webpack:
- Vite (most new projects)
- Turbopack (Next.js)
- esbuild (some projects)
Current status (2025):
- Still widely used (50% of projects)
- Webpack 5 stable
- Being replaced by Vite in new projects
- Still good for complex needs
- Not recommended for new projects unless specific needs
Rollup (2015)
Status: Popular for libraries
Problem it solved:
- 🎯 Webpack bundles were too large for libraries
- 🎯 Webpack's tree-shaking wasn't good enough
- 🎯 Libraries needed multiple output formats (ESM, CJS, UMD)
- 🎯 Webpack added too much runtime code (overhead)
- 🎯 ES6 modules needed a bundler that understood them natively
// rollup.config.js
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'esm' // or 'cjs', 'umd', 'iife'
},
plugins: [
resolve(),
commonjs(),
babel()
]
};
// Better tree-shaking than Webpack
// Smaller bundles
// ES modules native
Pros of Rollup:
- ✅ Better tree-shaking than Webpack
- ✅ Smaller bundles
- ✅ ES modules native
- ✅ Simpler config than Webpack
- ✅ Great for libraries
- ✅ Multiple output formats (ESM, CJS, UMD)
- ✅ Fast builds
Cons of Rollup:
- ❌ Not great for applications (use Vite)
- ❌ Less features than Webpack
- ❌ Smaller plugin ecosystem
- ❌ No HMR (use Vite for that)
- ❌ Code splitting less mature
When to use Rollup (2025):
- ✅ Building libraries (npm packages)
- ✅ Need multiple output formats
- ✅ Tree-shaking critical
- ❌ NOT for applications (use Vite)
Use case: Libraries (React, Vue, Svelte all use Rollup)
Current status (2025):
- Standard for library builds
- Used by Vite for production builds
- Not for applications directly
Parcel (2017)
Status: Niche/Declining
When it started declining: ~2020-2021
What replaced it: Vite
Problem it solved:
- 🎯 Webpack configuration was too complex (webpack.config.js hell)
- 🎯 Beginners struggled with build tools
- 🎯 Wanted "just work" experience (zero config)
- 🎯 Setting up Webpack + Babel + loaders took hours
- 🎯 Needed faster builds (multi-core processing)
# Before Parcel - Webpack config nightmare
// webpack.config.js (100+ lines)
// .babelrc
// package.json with 20+ dev dependencies
// Hours of setup
# With Parcel - Zero config
parcel index.html
# That's it! Automatically handles:
# - JavaScript (Babel)
# - TypeScript
# - CSS/SCSS
# - Images
# - Hot reload
Pros of Parcel:
- ✅ Zero config (truly zero)
- ✅ Fast (multi-core)
- ✅ Handles everything automatically
- ✅ Great for beginners
- ✅ Built-in dev server
- ✅ HMR out of the box
Cons of Parcel:
- ❌ Less control than Webpack
- ❌ Smaller ecosystem
- ❌ Some edge cases buggy
- ❌ Slower than Vite
- ❌ Less mature
- ❌ Configuration limited when needed
Why it didn't win:
- Webpack too entrenched (at the time)
- Less control for complex projects
- Vite came along (faster, better)
Current status (2025):
- Parcel 2 released but not popular
- Vite won the "fast bundler" race
- Not recommended (use Vite instead)
Vite (2020)
Status: Rising rapidly
Problem it solved:
- 🎯 Webpack dev server was SLOW (30+ seconds to start on large projects)
- 🎯 Hot Module Replacement was slow (seconds to see changes)
- 🎯 Rebuilding entire bundle on every change was wasteful
- 🎯 Browsers now support ES modules natively - why bundle in dev?
- 🎯 Developer experience was painful on large projects
The breakthrough:
- ✨ Dev: Use native ES modules (no bundling!) = instant start
- ✨ Prod: Use Rollup (optimized bundles)
- ✨ Pre-bundle dependencies with esbuild (100x faster than Webpack)
// Before Vite - Webpack dev server
npm run dev
// Wait 30-60 seconds...
// Make change
// Wait 3-5 seconds for HMR...
// With Vite
npm run dev
// Server starts in 200ms!
// Make change
// See update in 50ms!
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()]
});
// Dev server
vite
// Build
vite build
Pros of Vite:
- ✅ Extremely fast dev server (instant start)
- ✅ Lightning-fast HMR (instant updates)
- ✅ Native ESM in development
- ✅ Rollup for production (optimized)
- ✅ Simple configuration
- ✅ Great developer experience
- ✅ Plugin ecosystem growing
- ✅ TypeScript support
- ✅ CSS/Asset handling
- ✅ Framework agnostic
- ✅ Pre-bundling (esbuild)
Cons of Vite:
- ❌ Newer (less mature than Webpack)
- ❌ Some edge cases with legacy code
- ❌ Smaller plugin ecosystem than Webpack
- ❌ Dev/prod differences (ESM vs bundled)
- ❌ Some libraries don't work well with ESM
Why Vite is winning:
- 10-100x faster dev server than Webpack
- Instant HMR (no rebuild)
- Better developer experience
- Simple configuration
- Modern approach (ESM-first)
How it works:
// Development: Native ESM (no bundling!)
import { createApp } from 'vue';
// Browser loads this directly via HTTP/2
// Production: Rollup bundling
// Optimized, tree-shaken, code-split
When to use Vite (2025):
- ✅ New projects (highly recommended)
- ✅ Any framework (React, Vue, Svelte, etc.)
- ✅ Want fast development
- ✅ Modern browsers target
- ✅ TypeScript projects
Adoption:
- Vue 3 (official build tool)
- Svelte (SvelteKit)
- Solid.js (SolidStart)
- Qwik
- React (growing adoption)
- Recommended by most frameworks
Current status (2025):
- Rapidly growing (30% market share)
- Standard for new projects
- Recommended over Webpack
- Will likely become dominant
esbuild (2020)
Status: Used as build tool in other tools (not standalone)
Creator: Evan Wallace (Figma)
// Extremely fast (written in Go)
require('esbuild').build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js'
});
Pros of esbuild:
- ✅ Extremely fast (10-100x faster than Webpack)
- ✅ Written in Go (native performance)
- ✅ TypeScript/JSX built-in
- ✅ Tree shaking
- ✅ Minification
- ✅ Source maps
- ✅ Simple API
Cons of esbuild:
- ❌ Limited features (by design)
- ❌ No HMR
- ❌ No code splitting (initially)
- ❌ Not a complete build tool
- ❌ Limited plugin ecosystem
- ❌ Not meant for direct use
When to use esbuild (2025):
- ✅ As part of other tools (Vite uses it)
- ✅ Simple bundling needs
- ✅ Speed critical
- ❌ NOT for full applications (use Vite)
How it's used:
- Vite uses esbuild for pre-bundling dependencies
- Some projects use it directly for simple builds
- Building blocks for other tools
Current status (2025):
- Widely used indirectly (via Vite, etc.)
- Not meant as standalone build tool
- Proven technology
Turbopack (2022, Vercel)
Status: In development (beta)
Creator: Vercel (Tobias Koppers, creator of Webpack)
// Webpack successor (written in Rust)
// Used in Next.js 13+
// next dev --turbo
Pros of Turbopack:
- ✅ Extremely fast (written in Rust)
- ✅ Incremental computation (caches everything)
- ✅ Webpack successor (by same author)
- ✅ Designed for scale
- ✅ Vercel backing
Cons of Turbopack:
- ❌ Still experimental (not production-ready)
- ❌ Only for Next.js (currently)
- ❌ Not standalone yet
- ❌ Limited documentation
- ❌ Unproven at scale
When to use Turbopack (2025):
- ✅ Next.js 13+ (experimental)
- ❌ NOT for production yet
- ❌ NOT standalone
Current status (2025):
- Beta in Next.js
- Promising but not ready
- Wait for stable release
- Future looks bright
Bun (2023)
Status: All-in-one tool
# Bundler built-in
bun build ./index.tsx --outdir ./dist
# Extremely fast
# No configuration needed
Transpilers
Babel (2014-Present)
Status: Still essential
Problem it solved:
- 🎯 ES6 released but browsers didn't support it (2015)
- 🎯 Wanted to use new JavaScript features (arrow functions, classes, etc.)
- 🎯 Had to wait years for browser support
- 🎯 Different browsers supported different features
- 🎯 JSX needed transformation (React)
The breakthrough:
- ✨ Write modern JavaScript, run anywhere - Transpiles ES6+ to ES5
- ✨ Plugin system - Transform any syntax
- ✨ Polyfills - Add missing features (Promise, Array.from, etc.)
- ✨ JSX support - Transform React JSX to JavaScript
- ✨ Target specific browsers - Only transpile what's needed
// 2015 - Problem: Browsers don't support ES6
// Write this (ES6)
const add = (a, b) => a + b;
class Person {
constructor(name) {
this.name = name;
}
}
// But browsers only understand this (ES5)
var add = function(a, b) {
return a + b;
};
function Person(name) {
this.name = name;
}
// Babel solves this - Write ES6, Babel converts to ES5
// You write modern code, users get compatible code
Evolution:
// 2015: Babel 6 - Modular
{
"presets": ["es2015", "react"],
"plugins": ["transform-class-properties"]
}
// 2018: Babel 7 - @babel/
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead"
}],
"@babel/preset-react"
]
}
// What it does
// Input (ES6+)
const add = (a, b) => a + b;
class Person {
name = "John";
}
// Output (ES5)
var add = function(a, b) {
return a + b;
};
var Person = function Person() {
this.name = "John";
};
Why still needed:
- Browser compatibility
- JSX transformation
- TypeScript (alternative to tsc)
- Experimental features
SWC (2020)
Status: Growing (Rust-based, faster)
// 20x faster than Babel
// Used by Next.js, Parcel, Deno
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
},
"transform": {
"react": {
"runtime": "automatic"
}
}
}
}
Task Runners
Task Runners: Grunt (2012) → Gulp (2013) → npm scripts
Grunt (2012)
Status: Dead
When it became legacy: ~2015-2016
What replaced it: Gulp, then npm scripts
Problem it solved:
- 🎯 No standard way to automate build tasks (minify, concat, compile)
- 🎯 Had to manually run multiple commands
- 🎯 Build scripts were bash/shell scripts (not cross-platform)
- 🎯 Needed task automation in JavaScript
// Before Grunt - Manual commands
uglifyjs app.js -o app.min.js
sass styles.scss styles.css
imagemin images/* -o dist/images
// Run each command manually!
// With Grunt - Automated tasks
module.exports = function(grunt) {
grunt.initConfig({
uglify: {
build: {
src: 'app.js',
dest: 'app.min.js'
}
},
sass: {
dist: {
files: {
'styles.css': 'styles.scss'
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.registerTask('default', ['uglify', 'sass']);
};
// Run all tasks
grunt
Pros of Grunt:
- ✅ First popular task runner
- ✅ Large plugin ecosystem
- ✅ Configuration-based (declarative)
- ✅ Easy to understand
Cons of Grunt:
- ❌ Slow (file I/O heavy)
- ❌ Verbose configuration
- ❌ Temporary files everywhere
- ❌ Not streaming
- ❌ Complex for simple tasks
Current status: Dead, don't use
Gulp (2013)
Status: Legacy/Declining
When it became legacy: ~2017-2018
What replaced it: npm scripts, build tools (Webpack, Vite)
Problem it solved:
- 🎯 Grunt was too slow (wrote temp files to disk constantly)
- 🎯 Grunt configuration was verbose and hard to read
- 🎯 Wanted streaming (in-memory processing)
- 🎯 Needed code over configuration (more flexible)
// Grunt - Configuration-based (slow, verbose)
grunt.initConfig({
uglify: {
build: {
src: 'app.js',
dest: 'app.min.js'
}
}
});
// Writes temp files to disk
// Gulp - Code-based (fast, streaming)
const gulp = require('gulp');
const uglify = require('gulp-uglify');
const sass = require('gulp-sass');
gulp.task('scripts', function() {
return gulp.src('src/**/*.js')
.pipe(uglify()) // In-memory streaming
.pipe(gulp.dest('dist'));
});
gulp.task('styles', function() {
return gulp.src('src/**/*.scss')
.pipe(sass())
.pipe(gulp.dest('dist'));
});
gulp.task('default', gulp.parallel('scripts', 'styles'));
Pros of Gulp:
- ✅ Streaming (faster than Grunt)
- ✅ Code over configuration
- ✅ Simpler than Grunt
- ✅ Good plugin ecosystem
- ✅ Flexible
Cons of Gulp:
- ❌ Another abstraction layer
- ❌ npm scripts can do most things
- ❌ Build tools handle bundling better
- ❌ Maintenance overhead
- ❌ Less relevant with modern tools
Current status: Still used in some legacy projects, not recommended
npm scripts (2010-Present)
Status: Standard
What replaced: Grunt, Gulp
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"lint": "eslint src",
"format": "prettier --write src",
"preview": "vite preview"
}
}
Pros of npm scripts:
- ✅ No extra dependencies
- ✅ Simple and direct
- ✅ Standard across projects
- ✅ Easy to understand
- ✅ No abstraction layer
- ✅ Works with any tool
Cons of npm scripts:
- ❌ Limited for complex tasks
- ❌ Cross-platform issues (Windows vs Unix)
- ❌ No streaming
- ❌ Can get messy for complex workflows
When to use (2025):
- ✅ Always (standard approach)
- ✅ Simple task running
- ✅ Calling build tools
Modern approach:
- npm/pnpm/yarn scripts for task running
- Build tools (Vite, Webpack) for bundling
- No need for Grunt/Gulp
Package Management
npm (2010-Present)
Status: Default, still dominant
Problem it solved:
- 🎯 No standard way to share JavaScript libraries
- 🎯 Had to manually download and include libraries
- 🎯 No dependency management (had to track versions manually)
- 🎯 No way to publish/discover packages
- 🎯 Dependency hell (conflicting versions)
The breakthrough:
- ✨ Central registry - One place for all packages (npmjs.com)
- ✨ Automatic dependencies - Install package + all its dependencies
- ✨ Version management - Semantic versioning (^, ~, exact)
- ✨ Scripts - Run tasks (npm run build, npm test)
- ✨ Largest ecosystem - 2+ million packages
// Before npm - Manual download
// 1. Go to jQuery website
// 2. Download jquery.min.js
// 3. Copy to project
// 4. Add <script src="jquery.min.js"></script>
// 5. Repeat for every library
// 6. Track versions manually
// 7. Update manually
// With npm - Automatic
npm install jquery
// Downloads jquery + all dependencies
// Tracks versions in package.json
// Updates with npm update
# Evolution
npm install express # 2010
npm install --save express # 2012-2016
npm install express # 2017+ (--save default)
npm ci # 2018 (clean install for CI)
# package.json
{
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
# package-lock.json (2016)
# Ensures consistent installs
Yarn (2016)
Status: Still popular
Problem it solved:
- 🎯 npm was slow (sequential installs)
- 🎯 npm was non-deterministic (different installs could differ)
- 🎯 No offline mode
- 🎯 Security concerns (no checksums)
- 🎯 Poor monorepo support
The breakthrough:
- ✨ Parallel installs - Much faster than npm
- ✨ Lockfile - Deterministic installs (yarn.lock)
- ✨ Offline cache - Install without internet
- ✨ Workspaces - Better monorepo support
- ✨ Security - Checksums for packages
# npm (2016) - Slow
npm install
# Takes 2-3 minutes, sequential
# Yarn (2016) - Fast
yarn install
# Takes 30 seconds, parallel!
# Yarn 1 (2016) - Faster than npm
yarn add express
# Yarn 2/3 (Berry) (2020) - Plug'n'Play
yarn set version berry
# .yarnrc.yml
nodeLinker: pnp # No node_modules!
# Workspaces
{
"workspaces": ["packages/*"]
}
Why Yarn:
- Faster (initially)
- Workspaces (monorepos)
- Offline mode
- Deterministic
pnpm (2017)
Status: Growing rapidly
# Efficient disk usage (hard links)
pnpm install express
# Strict node_modules structure
# Prevents phantom dependencies
# Workspaces
{
"pnpm": {
"workspaces": ["packages/*"]
}
}
Why pnpm is winning:
- Saves disk space (shared dependencies)
- Faster than npm
- Strict (catches bugs)
- Great for monorepos
Disk usage comparison:
- npm: 1 GB for 3 projects
- Yarn: 1 GB for 3 projects
- pnpm: 350 MB for 3 projects
Bun (2023)
Status: Fastest
# Extremely fast
bun install
# Compatible with npm
# 10-20x faster
Current Landscape (2025)
Frontend Frameworks
Market Share (approximate):
-
React (40%) - Still dominant
- Huge ecosystem
- Job market leader
- Meta backing
- Next.js standard
-
Vue (20%) - Strong second
- Easier learning curve
- Great documentation
- Popular in Asia
- Nuxt.js mature
-
Angular (15%) - Enterprise
- TypeScript native
- Complete framework
- Enterprise adoption
- Google backing
-
Svelte (10%) - Rising
- Best DX
- Smallest bundles
- SvelteKit mature
- Growing fast
-
Others (15%)
- Solid.js (performance)
- Qwik (resumability)
- Preact (React alternative)
- Alpine.js (lightweight)
Meta-Frameworks (Recommended)
Don't use frameworks alone anymore!
- React → Next.js or Remix
- Vue → Nuxt.js
- Svelte → SvelteKit
- Solid → SolidStart
- Angular → Analog (new)
Why meta-frameworks:
- SSR/SSG out of the box
- Routing
- API routes
- Optimizations
- Better DX
State Management (2025)
React:
- TanStack Query - Server state
- Zustand - Client state (simple)
- Jotai - Atomic state
- Redux Toolkit - Complex apps
- Context + Hooks - Simple apps
Vue:
- Pinia - Official (replaced Vuex)
Angular:
- NgRx - Redux-like
- Signals - Built-in (Angular 16+)
Build Tools (2025)
Recommended:
- Vite - Modern projects
- Webpack - Legacy/complex needs
- Turbopack - Next.js
- Bun - All-in-one
For libraries:
- Rollup or tsup
Testing (2025)
Unit/Integration:
- Vitest - Modern (Vite projects)
- Jest - Established (React)
- Bun Test - Bun projects
E2E:
- Playwright - Best overall
- Cypress - Great DX
Package Managers (2025)
Recommended:
- pnpm - Best for most
- npm - Default, reliable
- Yarn - Workspaces
- Bun - Speed
TypeScript (2025)
Status: Industry standard
Adoption:
- 80%+ of new projects
- Required for most jobs
- All major frameworks support it
- Better tooling
Recommendation: Use TypeScript for everything
Runtime (2025)
Server-side:
- Node.js - Standard
- Bun - Fast alternative
- Deno - Secure alternative
Edge:
- Cloudflare Workers
- Vercel Edge
- Deno Deploy
What to Learn in 2025
For Beginners
-
JavaScript fundamentals
- ES6+ features
- Async/await
- Modules
- Array methods
-
TypeScript basics
- Types, interfaces
- Generics
- Type inference
-
React + Next.js
- Hooks
- Server Components
- App Router
-
Modern tooling
- Vite
- pnpm
- Vitest
For Returning Developers
If you left in 2010 (jQuery era):
Learn:
- ES6+ (let/const, arrow functions, classes, modules)
- async/await
- React or Vue
- TypeScript
- Next.js or Nuxt.js
- Modern build tools (Vite)
If you left in 2015 (Early React):
Learn:
- React Hooks (replaced classes)
- TypeScript
- Server Components (Next.js 13+)
- Modern state management (TanStack Query, Zustand)
- Vite
If you left in 2020:
Learn:
- Server Components
- Signals (Angular, Solid)
- Bun/Deno
- Astro (for content sites)
- Playwright (testing)
Key Takeaways
What Died
- jQuery - Native APIs good enough
- AngularJS 1.x - Replaced by Angular 2+
- Backbone.js - Too minimal
- Grunt/Gulp - npm scripts enough
- Moment.js - Too large, mutable
- Bower - npm won
- CoffeeScript - ES6+ made it obsolete
What Survived
- React - Adapted (Hooks, Server Components)
- Vue - Evolved (Composition API)
- Angular - Complete rewrite
- Node.js - Still standard
- npm - Still default
- Webpack - Still used (but challenged)
- TypeScript - Now essential
What's New and Hot
- Vite - Fast builds
- pnpm - Efficient packages
- Vitest - Fast testing
- Playwright - Best E2E
- Bun - Fast runtime
- Server Components - New paradigm
- Signals - Fine-grained reactivity
- Astro - Content sites
The Biggest Changes
- TypeScript everywhere (2012 → 2025)
- Frameworks required (jQuery → React/Vue/Angular)
- Build tools essential (None → Webpack → Vite)
- Server-side rendering (SPA → SSR/SSG)
- Component-based (jQuery → Components)
- Type safety (Dynamic → Static typing)
- Modern JavaScript (ES5 → ES2025)
- Testing standard (Optional → Required)
- Monorepos common (Single → Workspaces)
- Edge computing (Server → Edge)
Conclusion
JavaScript has evolved from a simple scripting language to a comprehensive ecosystem powering everything from websites to servers to mobile apps to desktop applications.
The journey:
- 1995-2005: Birth and browser wars
- 2005-2009: AJAX and jQuery revolution
- 2009-2015: Node.js and modern frameworks
- 2015-2020: ES6+ and React dominance
- 2020-2025: TypeScript, Server Components, and performance
The future:
- More server-side rendering
- Better performance (Signals, Resumability)
- Type safety everywhere
- Edge computing
- AI integration
- WebAssembly integration
For developers returning to JavaScript: The ecosystem is more mature, more powerful, and more complex. But the fundamentals remain: build great user experiences, write maintainable code, and keep learning.
Welcome back to JavaScript! 🎉
Technology Status Summary Table
Frontend Frameworks
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| jQuery | Legacy | 2017-2018 | Native APIs, React, Vue | ❌ Don't use for new projects |
| Backbone.js | Dead | 2015-2016 | React, Vue, Angular | ❌ Migrate away |
| Knockout.js | Legacy | 2016-2017 | Vue, React | ❌ Migrate away |
| AngularJS 1.x | Dead | 2016 (EOL 2022) | Angular 2+, React, Vue | ❌ Migrate immediately |
| Ember.js | Declining | 2015-2016 | React, Vue | ⚠️ Only if already using |
| React | Dominant | - | - | ✅ Highly recommended |
| Vue | Strong | - | - | ✅ Highly recommended |
| Angular 2+ | Stable | - | - | ✅ Good for enterprise |
| Svelte | Growing | - | - | ✅ Recommended for new projects |
| Solid.js | Niche | - | - | ⚠️ Experimental/side projects |
| Qwik | Experimental | - | - | ⚠️ Not production-ready |
Testing Frameworks
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| QUnit | Dead | 2014-2015 | Jest, Mocha | ❌ Don't use |
| Jasmine | Declining | 2016-2017 | Jest, Vitest | ⚠️ Only for Angular legacy |
| Mocha | Declining | 2017-2018 | Jest, Vitest | ⚠️ Legacy projects only |
| Jest | Dominant | - | Vitest (for Vite) | ✅ Standard for React |
| Vitest | Growing | - | - | ✅ Recommended for Vite projects |
| Bun Test | Experimental | - | - | ⚠️ Not production-ready |
E2E Testing
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Selenium | Legacy | 2017-2018 | Playwright, Cypress | ⚠️ Only for legacy/multi-language |
| Protractor | Dead | 2020 (EOL 2022) | Playwright, Cypress | ❌ Migrate immediately |
| Cypress | Popular | - | Playwright (partially) | ✅ Good for SPAs |
| Playwright | Growing | - | - | ✅ Recommended for new projects |
| Puppeteer | Niche | - | - | ✅ For automation, not testing |
Build Tools
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Browserify | Dead | 2015-2016 | Webpack, Rollup | ❌ Don't use |
| Webpack | Mature | - | Vite (for apps) | ⚠️ Legacy projects, complex needs |
| Rollup | Stable | - | - | ✅ For libraries only |
| Parcel | Declining | 2020-2021 | Vite | ❌ Use Vite instead |
| Vite | Growing | - | - | ✅ Highly recommended for apps |
| esbuild | Tool | - | - | ✅ As part of other tools |
| Turbopack | Experimental | - | - | ⚠️ Not production-ready |
Task Runners
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Grunt | Dead | 2015-2016 | Gulp, npm scripts | ❌ Don't use |
| Gulp | Legacy | 2017-2018 | npm scripts | ⚠️ Legacy projects only |
| npm scripts | Standard | - | - | ✅ Always use |
Package Managers
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Bower | Dead | 2015 | npm | ❌ Don't use |
| npm | Standard | - | - | ✅ Default, reliable |
| Yarn | Stable | - | - | ✅ Good for monorepos |
| pnpm | Growing | - | - | ✅ Recommended for most |
| Bun | Experimental | - | - | ⚠️ Experimental |
Meta-Frameworks
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Next.js | Dominant | - | - | ✅ Standard for React |
| Nuxt.js | Strong | - | - | ✅ Standard for Vue |
| Remix | Growing | - | - | ✅ Good alternative to Next.js |
| SvelteKit | Stable | - | - | ✅ Standard for Svelte |
| Astro | Growing | - | - | ✅ Great for content sites |
State Management (React)
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Redux | Mature | - | Redux Toolkit | ⚠️ Use Redux Toolkit instead |
| Redux Toolkit | Standard | - | - | ✅ For complex state |
| MobX | Declining | - | Zustand, Jotai | ⚠️ Legacy projects only |
| Context API | Built-in | - | - | ✅ For simple state |
| Zustand | Growing | - | - | ✅ Recommended for client state |
| Jotai | Growing | - | - | ✅ Recommended for atomic state |
| TanStack Query | Standard | - | - | ✅ Required for server state |
Utility Libraries
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Lodash | Stable | - | Native JS (partial) | ✅ Still useful |
| Underscore | Legacy | 2015 | Lodash | ❌ Use Lodash |
| Moment.js | Deprecated | 2020 | date-fns, Day.js | ❌ Migrate away |
| date-fns | Standard | - | - | ✅ Recommended |
| Day.js | Standard | - | - | ✅ Recommended (smaller) |
Runtimes
| Technology | Status | Legacy Since | Replaced By | Recommendation 2025 |
|---|---|---|---|---|
| Node.js | Standard | - | - | ✅ Industry standard |
| Deno | Niche | - | - | ⚠️ Experimental/specific use cases |
| Bun | Experimental | - | - | ⚠️ Promising but not production-ready |
Quick Decision Guide (2025)
Starting a New Project?
Frontend Framework:
- React → Use Next.js (most jobs, largest ecosystem)
- Vue → Use Nuxt.js (easier learning curve, great DX)
- Svelte → Use SvelteKit (best DX, smallest bundles)
- Angular → Only if enterprise/large team
Build Tool:
- Vite (recommended for 90% of projects)
- Webpack (only if specific needs)
Testing:
- Unit: Vitest (if using Vite) or Jest (if React)
- E2E: Playwright (recommended) or Cypress (if prefer DX)
Package Manager:
- pnpm (recommended for most)
- npm (default, reliable)
- yarn (if need workspaces)
State Management (React):
- Server state: TanStack Query (required)
- Client state: Zustand (simple) or Jotai (atomic)
- Complex: Redux Toolkit
TypeScript:
- ✅ Always use TypeScript (industry standard)
Migration Priority Guide
Migrate Immediately (Dead/EOL)
- AngularJS 1.x → Angular/React/Vue (EOL 2022)
- Protractor → Playwright/Cypress (EOL 2022)
- Moment.js → date-fns/Day.js (deprecated)
- Bower → npm/pnpm (dead)
- Grunt → npm scripts (dead)
Migrate Soon (Legacy)
- jQuery → React/Vue + Native APIs
- Backbone/Knockout → Modern framework
- Mocha/Jasmine → Jest/Vitest
- Selenium → Playwright (if JavaScript)
- Gulp → npm scripts
- Webpack → Vite (for new features)
Can Wait (Still Supported)
- Redux → Redux Toolkit (same ecosystem)
- Jest → Vitest (if moving to Vite)
- Cypress → Playwright (both good)
- npm → pnpm (both work fine)
Last updated: December 4, 2025 - 30 Years of JavaScript
Top comments (3)
Greatest collection of JS ecosystem by history, I just miss the jsDock because I think that can be a better typesafe option compare to typescript: jsDoc evangelism
I actually think thet Dart should have been the better option but the Angular team chosed TS and basically kicking the TS revolution and kinda killing their own colleges in the meantime :(
P.s. that's the main reason for me not liking angular :(
I am a JS fanatic special after ES6. That why great the jsDoc because that is don't want modify to code just give a type hint under development.
My main missing from javascript is a simple pipeline operator. I worked with optional-pipeline-operator ( babel ) with 2 years and that is will be really hugh improvement of JS. Saddly cannot able to agree with a simple pipeline where just one parameter eneabled.