DOCODIVE
Advanced Free Learning Path

Web Development Advanced Course

Master the deepest parts of frontend engineering — advanced CSS, JavaScript internals, real-time APIs, performance, and full app architecture. This is where you go from writing code to engineering software.

8–12 weeks 40 lessons 1 capstone Intermediate knowledge required
Start Learning
01

Advanced CSS Grid: Named Areas & minmax

20 min
What you'll learn
  • Master grid-template-areas
  • Use minmax() and auto-fit
  • Build complex page layouts

You already know basic Grid — now it's time for production-grade layouts. grid-template-areas lets you name regions and place them visually, so your CSS reads like a wireframe. minmax() and auto-fit together create grids that automatically flow as many columns as fit — the magic behind responsive galleries and dashboards that adapt without a single media query.

grid-areas.css
.layout {
  display: grid;
  grid-template-areas:
    'header header header'
    'sidebar main main'
    'footer footer footer';
  grid-template-columns: 200px 1fr 1fr;
  gap: 12px;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Live Preview
browser — Advanced CSS Grid: Named Areas & minmax
1
2
3
4
5
6
✓ Best Practice: grid-template-areas is the single biggest Grid productivity boost — map your layout visually.
Try it yourself

Build a layout with header, two equal columns, and footer using grid-template-areas.

Use named areas and grid-template-columns: 1fr 1fr.
.l { display:grid; grid-template-areas:'h h' 'a b' 'f f'; grid-template-columns:1fr 1fr; }
02

CSS Container Queries

20 min
What you'll learn
  • Understand container queries
  • Style based on parent size
  • Build reusable components

Media queries respond to the viewport; container queries respond to a parent's size. This is a game-changer for components: a card can change its layout based on how much room IT has, not how wide the screen is. Write a component once, drop it anywhere, and it adapts automatically. This is the modern way to build truly reusable UI.

container.css
.card-container {
  container-type: inline-size;
}
.card {
  display: block;
}
@container (min-width: 400px) {
  .card {
    display: flex;
    gap: 16px;
  }
}
Live Preview
browser — CSS Container Queries
📦 Container A
📦 Container B
📦 Container C
🔎 Important: Container queries finally make 'component-driven responsive design' real — no more viewport-only thinking.
Try it yourself

Make a component switch from column to row at 350px container width.

container-type: inline-size + @container (min-width: 350px).
.wrap { container-type: inline-size; }
@container (min-width:350px) { .item { flex-direction: row; } }
03

Fluid Typography with clamp()

16 min
What you'll learn
  • Use clamp() for fluid sizes
  • Scale typography smoothly
  • Build readable responsive text

clamp() picks a value between a minimum and maximum, with a preferred size in between. For typography, clamp(1rem, 2vw + 1rem, 2rem) makes text grow smoothly with the screen — no breakpoints, no sudden jumps. It's the modern replacement for dozens of media-query font-size overrides and keeps reading comfortable on every device.

fluid.css
h1 {
  font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
}
p {
  font-size: clamp(1rem, 1vw + 0.75rem, 1.25rem);
  line-height: 1.6;
}
Live Preview
browser — Fluid Typography with clamp()

Fluid Heading

This text scales smoothly with the viewport using clamp().

✓ Best Practice: clamp() is the easiest win in responsive design — one line replaces a whole stack of breakpoints.
Try it yourself

Make a heading that scales from 1.2rem to 2.4rem.

clamp(1.2rem, 3vw, 2.4rem).
h1 { font-size: clamp(1.2rem, 3vw + 0.5rem, 2.4rem); }
04

Advanced Animations: Staggering & Easing

22 min
What you'll learn
  • Create staggered entrances
  • Use custom cubic-bezier easing
  • Build polished motion

Great animation is about timing. Staggering — delaying each item slightly — turns a flat list into an elegant cascade. cubic-bezier() gives you custom easing curves that feel natural instead of robotic. The difference between a site that feels cheap and one that feels premium is often just these two details applied with restraint.

stagger.css
.item {
  opacity: 0;
  animation: fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }
.item:nth-child(4) { animation-delay: 0.3s; }
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}
Live Preview
browser — Advanced Animations: Staggering & Easing

Staggered entrance

💡 Tip: That cubic-bezier(0.16,1,0.3,1) is the famous 'ease-out-expo' — feels premium on entrances.
Try it yourself

Stagger 5 list items by 0.05s each.

nth-child with increasing delays.
.li:nth-child(1){animation-delay:0s}.li:nth-child(2){animation-delay:.05s}/* etc */
05

CSS Scroll Snap

16 min
What you'll learn
  • Create snap-scrolling carousels
  • Use scroll-snap-type
  • Build touch-friendly galleries

Scroll snap makes content settle neatly into positions as users scroll — the backbone of carousels, galleries, and onboarding screens. The container declares scroll-snap-type, each item declares scroll-snap-align. The browser does the rest with smooth native scrolling, no JavaScript required. It's one of the most underrated modern CSS features.

snap.css
.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.carousel-item {
  flex: 0 0 100%;
  scroll-snap-align: center;
}
Live Preview
browser — CSS Scroll Snap
Slide 1
Slide 2
Slide 3
✓ Best Practice: Native scroll snap beats JS carousels on mobile — it respects touch physics and momentum.
Try it yourself

Create a vertical snap container with three sections.

scroll-snap-type: y mandatory.
.c { overflow-y:auto; scroll-snap-type:y mandatory; height:100vh; }
.s { scroll-snap-align:start; height:100vh; }
06

CSS Masking & Clipping

18 min
What you'll learn
  • Clip with clip-path
  • Mask with images
  • Create creative shapes

clip-path and mask let you cut elements into shapes — circles, polygons, even custom SVG paths. This powers everything from circular avatars to decorative section dividers. Unlike border-radius (which only rounds corners), clip-path can create any polygon shape, and masks can fade content out with gradients. It's how designers make pages feel bespoke.

mask.css
.circle {
  width: 120px;
  height: 120px;
  background: linear-gradient(135deg, #e11d48, #f59e0b);
  clip-path: circle(50%);
}
.hex {
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
}
Live Preview
browser — CSS Masking & Clipping
💡 Tip: clip-path: circle(50%) is the cleanest way to make perfect circular avatars.
Try it yourself

Clip an element into a triangle.

clip-path: polygon(50% 0%, 0% 100%, 100% 100%).
.tri { clip-path: polygon(50% 0%, 0% 100%, 100% 100%); }
07

CSS Logical Properties

14 min
What you'll learn
  • Write RTL-friendly CSS
  • Use margin-inline and padding-block
  • Build international sites

Logical properties describe spacing and sizing relative to text direction, not physical left/right. margin-inline-start works on the correct side whether text flows left-to-right (English) or right-to-left (Arabic). If you build for international audiences, this is the difference between a site that works everywhere and one that breaks the moment it's translated.

logical.css
.box {
  padding-inline: 20px;
  padding-block: 12px;
  margin-inline-start: auto;
  border-inline-start: 4px solid #e11d48;
}
Live Preview
browser — CSS Logical Properties
Themed Button
🔎 Important: If your audience might include Arabic or Hebrew readers, logical properties are non-negotiable.
Try it yourself

Replace margin-left with a logical property.

margin-inline-start.
.x { margin-inline-start: 16px; }
08

Pseudo-elements ::before and ::after

16 min
What you'll learn
  • Add decorative content
  • Create tooltips and badges
  • Avoid extra HTML

::before and ::after generate virtual elements without touching your HTML. They're perfect for decorative dots, icons, badges, tooltips, and animated underlines. Because they're pure CSS, they keep your markup clean and semantic. Master them and you'll find yourself writing less HTML and achieving more polished details.

pseudo.css
.badge::after {
  content: 'NEW';
  position: absolute;
  top: -8px;
  right: -8px;
  background: #e11d48;
  color: white;
  font-size: 0.6rem;
  padding: 2px 6px;
  border-radius: 50px;
}
Live Preview
browser — Pseudo-elements ::before and ::after
Product NEW
💡 Tip: Pseudo-elements must have content:'' (even empty) or they won't render at all.
Try it yourself

Add a small circle bullet before every list item using ::before.

content:''; width:6px; height:6px; background:red; border-radius:50%.
li::before { content:''; display:inline-block; width:6px; height:6px; background:#e11d48; border-radius:50%; margin-right:6px; }
09

CSS Theming with Custom Properties

18 min
What you'll learn
  • Build dark/light themes
  • Create theme tokens
  • Apply system preference

A theming system lets users switch between dark and light mode — or follow their OS preference automatically. You define semantic tokens (--bg, --text, --accent) at the :root level, then swap them inside a [data-theme='dark'] attribute selector. prefers-color-scheme detects the OS choice. This is how every professional site handles theming.

theming.css
:root {
  --bg: #ffffff;
  --text: #1e1b4b;
  --accent: #e11d48;
}
[data-theme='dark'] {
  --bg: #0f172a;
  --text: #f1f5f9;
  --accent: #fb7185;
}
body {
  background: var(--bg);
  color: var(--text);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) { --bg: #0f172a; --text: #f1f5f9; }
}
Live Preview
browser — CSS Theming with Custom Properties
Themed Button
✓ Best Practice: Design tokens in CSS variables are the foundation of every serious design system.
Try it yourself

Add a --spacing token and use it for card padding.

:root { --spacing: 16px; } .card { padding: var(--spacing); }
:root { --spacing: 16px; }
.card { padding: var(--spacing); }
10

CSS Performance: contain & will-change

16 min
What you'll learn
  • Understand rendering cost
  • Use contain to isolate layout
  • Apply will-change wisely

CSS performance matters at scale. contain tells the browser an element's styles don't affect anything outside it, so the renderer can skip expensive recalcs. will-change hints which properties will animate, so the browser pre-optimizes. Both improve scroll and animation smoothness on content-heavy pages — and both are easy to overuse, so apply them only where you measure problems.

perf.css
.card {
  contain: layout paint;
  transition: transform 0.3s ease;
  will-change: transform;
}
.card:hover {
  transform: translateY(-4px);
}
Output
console
Cards that animate smoothly even among hundreds of siblings, because each is layout-isolated and pre-optimized for transform.
⚠️ Common Mistake: will-change is a hint, not a magic spell — overusing it consumes memory and can make things slower.
Try it yourself

Add will-change: transform to a card that will animate.

will-change: transform before the transition.
.card { will-change: transform; transition: transform .2s; }
11

Advanced Flexbox: flex-basis & order

16 min
What you'll learn
  • Control item sizing precisely
  • Reorder items visually
  • Build complex responsive rows

flex-basis sets an item's starting size before growing/shrinking; flex-grow distributes extra space; order changes visual position without touching HTML. Together they let you build layouts where a sidebar is 250px, the main content flexes to fill, and on mobile the whole order reflows — all with three properties and no media-query hacks.

flex-advanced.css
.container {
  display: flex;
  gap: 12px;
}
.sidebar {
  flex: 0 0 250px;
}
.main {
  flex: 1 1 auto;
}
@media (max-width: 700px) {
  .sidebar { order: 2; }
  .main { order: 1; }
  .container { flex-direction: column; }
}
Live Preview
browser — Advanced Flexbox: flex-basis & order
MySite
HomeAbout
💡 Tip: order is powerful but affects keyboard/screen-reader order — use it carefully for accessibility.
Try it yourself

Make one item twice as wide as its siblings using flex-grow.

flex: 2 on the wider item, flex: 1 on others.
.a { flex: 2; } .b, .c { flex: 1; }
12

CSS 3D Transforms

20 min
What you'll learn
  • Rotate in 3D space
  • Use perspective
  • Build flip cards

3D transforms add depth — rotateY, rotateX, and perspective make elements feel like physical cards. The classic flip card is built by rotating a front and back face 180 degrees apart. Perspective on the parent creates the depth illusion. It's flashy, so use it sparingly — but for profile cards and product images, it's delightful.

3d.css
.flip-card {
  perspective: 1000px;
}
.flip-inner {
  transition: transform 0.6s;
  transform-style: preserve-3d;
}
.flip-card:hover .flip-inner {
  transform: rotateY(180deg);
}
.flip-front, .flip-back {
  backface-visibility: hidden;
}
.flip-back {
  transform: rotateY(180deg);
}
Live Preview
browser — CSS 3D Transforms
Front
Back
💡 Tip: backface-visibility: hidden is the secret that makes both faces render correctly.
Try it yourself

Add a rotateX tilt effect on hover.

:hover { transform: rotateX(10deg); }
.tilt:hover { transform: perspective(500px) rotateX(10deg); }
13

JavaScript Closures Deep Dive

22 min
What you'll learn
  • Understand lexical scope
  • Create private state
  • Build factory functions

A closure is a function that remembers the variables from where it was created, even after that outer function has finished. This is how JavaScript achieves private state without classes — the inner function 'closes over' the outer variables. Closures power callbacks, event handlers, factories, and the module pattern. It's arguably the single most important JavaScript concept to truly internalize.

closures.js
function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Live Preview
browser — JavaScript Closures Deep Dive
Outercount=0
Innercount++
🔎 Important: Each call to createCounter() makes a NEW independent count — closures preserve their own state.
Try it yourself

Create a makeAdder(n) that returns a function adding n to its argument.

Return x => x + n.
function makeAdder(n) { return x => x + n; }
const add5 = makeAdder(5);
add5(3); // 8
14

Prototypes & Prototypal Inheritance

20 min
What you'll learn
  • Understand the prototype chain
  • Use Object.create
  • Grasp inheritance without classes

JavaScript uses prototypal inheritance, not classical. Every object has a hidden link to a prototype object, and property lookups walk that chain. This is how 'methods' work — they're just properties found on the prototype. Classes are syntax sugar over this system, so understanding prototypes makes the class behavior finally click.

prototype.js
const animal = {
  speak() { return 'Sound'; }
};
const dog = Object.create(animal);
dog.speak = function() { return 'Woof'; };

console.log(dog.speak());
console.log(Object.getPrototypeOf(dog) === animal);
Output
console
Woof true
🔎 Important: Object.getPrototypeOf() is your debugging friend — it reveals the hidden prototype link.
Try it yourself

Create two objects where one inherits from the other, then override a method.

Object.create(parent).
const parent = { greet(){return 'hi'} };
const child = Object.create(parent);
child.greet = () => 'hello';
child.greet(); // 'hello'
15

The Event Loop & Microtasks

22 min
What you'll learn
  • Understand call stack and task queue
  • Distinguish microtasks from macrotasks
  • Predict async execution order

JavaScript is single-threaded, yet it handles async — thanks to the event loop. Synchronous code runs first, Promises (microtasks) run before setTimeout (macrotasks). Understanding this ordering explains the most confusing interview questions and real bugs. Once you know microtasks jump the queue, async behavior stops being mysterious.

event-loop.js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
Output
console
1 4 3 2
🔎 Important: Microtasks (Promises) run before macrotasks (setTimeout), even if the timeout is 0ms.
Try it yourself

Predict: console.log('a'); setTimeout(()=>console.log('b')); Promise.resolve().then(()=>console.log('c'));

Sync → microtask → macrotask.
a, c, b
16

Advanced Promises: allSettled, any, race

20 min
What you'll learn
  • Combine multiple promises
  • Handle partial failures
  • Pick the fastest result

Beyond the basics, Promise combinators handle real-world concurrency. Promise.all rejects if ANY promise fails — brittle for independent tasks. Promise.allSettled waits for all and reports each success/failure. Promise.any resolves with the first success, Promise.race with the first settlement. Choosing the right combinator is what separates robust async code from fragile code.

promises.js
const tasks = [
  fetch('/api/a'),
  fetch('/api/b'),
  fetch('/api/c')
];
const results = await Promise.allSettled(tasks);
results.forEach((r, i) => {
  console.log(i, r.status, r.value || r.reason);
});
Output
console
0 fulfilled Response 1 rejected TypeError 2 fulfilled Response
✓ Best Practice: Use allSettled when independent requests shouldn't fail each other — it's the resilient default.
Try it yourself

Use Promise.any to fetch from two mirrors and use whichever succeeds first.

Promise.any([fetch(a), fetch(b)]).
const res = await Promise.any([fetch(mirror1), fetch(mirror2)]);
17

Generators & Iterators

20 min
What you'll learn
  • Create iterable objects
  • Use function* and yield
  • Build lazy sequences

Generators (function*) produce values lazily with yield, pausing between each one. Unlike an array that computes everything eagerly, a generator produces the next value only when asked. This enables infinite sequences, memory-efficient data processing, and custom iteration. They're niche but powerful — essential for anyone writing libraries or processing large datasets.

generators.js
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
Output
console
0 1 1 2
💡 Tip: Generators shine with infinite or huge sequences — they compute one value at a time, not the whole list.
Try it yourself

Write a generator that yields even numbers forever.

let n = 0; while(true) { yield n; n += 2; }
function* evens(){ let n=0; while(true){ yield n; n+=2; } }
18

Proxy & Reflect

22 min
What you'll learn
  • Intercept object operations
  • Build reactive data
  • Understand meta-programming

A Proxy wraps an object and intercepts operations — getting, setting, deleting properties. This is how reactivity systems (like Vue's) detect changes. Reflect provides the default behavior cleanly. Proxies let you build validation, logging, and 'auto-saving' objects. It's advanced meta-programming, but understanding it reveals how modern frameworks work under the hood.

proxy.js
const target = { name: 'Sufyan' };
const handler = {
  set(obj, prop, value) {
    console.log(`Setting ${prop} to ${value}`);
    return Reflect.set(obj, prop, value);
  }
};
const proxy = new Proxy(target, handler);
proxy.name = 'Ali'; // logs 'Setting name to Ali'
Output
console
Setting name to Ali
🔎 Important: Proxies are invisible — code using the proxy doesn't know it's wrapped, which is both the power and the danger.
Try it yourself

Create a proxy that logs every property read.

Use the get trap.
const h = { get(o,p){ console.log('read',p); return o[p]; } };
const p = new Proxy({a:1}, h);
p.a; // logs 'read a'
19

Symbols & Well-known Symbols

16 min
What you'll learn
  • Create unique keys
  • Avoid property collisions
  • Customize built-in behavior

Symbols are guaranteed-unique values, perfect for object keys that must never collide. Well-known symbols like Symbol.iterator let you customize how built-in operations treat your objects — defining Symbol.iterator on an object makes it work with for...of. They're how JavaScript extends itself, and they power many advanced patterns.

symbols.js
const id = Symbol('id');
const user = { [id]: 123, name: 'Sufyan' };

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1; yield 2; yield 3;
  }
};
console.log([...iterable]); // [1, 2, 3]
Output
console
[1, 2, 3]
💡 Tip: Symbol keys don't show up in Object.keys() — useful for 'hidden' internal properties.
Try it yourself

Create a unique Symbol and use it as an object key.

const k = Symbol(); obj[k] = ...
const k = Symbol('key');
const o = {}; o[k] = 'value';
o[k]; // 'value'
20

Map, Set, WeakMap, WeakSet

20 min
What you'll learn
  • Use Map for key-value data
  • Use Set for unique values
  • Understand weak collections

Map stores key-value pairs with any key type (not just strings like objects). Set stores unique values and answers 'contains?' instantly. WeakMap/WeakSet hold weak references — entries vanish when nothing else uses them, which prevents memory leaks for DOM-tracking and caches. These four are the modern replacements for object-as-dictionary and array.includes hacks.

collections.js
const map = new Map();
map.set('user', { name: 'Sufyan' });
map.set(1, 'one');

const set = new Set([1, 2, 2, 3]);
console.log(map.get('user'), set.size, set.has(2));
Output
console
{ name: 'Sufyan' } 3 true
✓ Best Practice: Use Map for frequent add/delete and non-string keys; Set for deduplication and membership tests.
Try it yourself

Use a Set to remove duplicates from [1,1,2,3,3].

[...new Set(arr)].
const arr = [1,1,2,3,3];
const unique = [...new Set(arr)]; // [1,2,3]
21

ES Modules

18 min
What you'll learn
  • Use import/export
  • Organize code into modules
  • Understand tree-shaking

ES modules split code into files that explicitly import and export what they need. Named exports share multiple values; default exports share one. Modules enable tree-shaking — bundlers can drop unused code, shrinking your final bundle. This is the foundation of modern frontend architecture, and the standard everywhere from Node to browsers to bundlers.

modules.js
// utils.js
export function add(a, b) { return a + b; }
export const PI = 3.14159;

// main.js
import { add, PI } from './utils.js';
console.log(add(2, 3), PI);
Live Preview
browser — ES Modules
utils.js
main.js
✓ Best Practice: Named exports are tree-shakeable — if you import only add(), the bundler drops anything unused.
Try it yourself

Create a module exporting two functions and import one of them.

export function... import { one } from...
// a.js
export const f1 = () => 1;
export const f2 = () => 2;
// b.js
import { f1 } from './a.js';
22

Debounce & Throttle

20 min
What you'll learn
  • Limit expensive function calls
  • Debounce input handlers
  • Throttle scroll events

Debounce and throttle control how often a function runs. Debounce waits until activity stops — perfect for search inputs (fire after the user pauses typing). Throttle enforces a maximum rate — perfect for scroll and resize handlers. Without them, you'd fire hundreds of expensive operations per second and tank performance.

debounce.js
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
const search = debounce((q) => console.log('Searching:', q), 500);
search('h'); search('he'); search('hel'); // only 'hel' logs after 500ms
Live Preview
browser — Debounce & Throttle
Waiting 500ms after typing stops…
🔎 Important: Debounce is non-negotiable for search-as-you-type — every keystroke would otherwise hit your API.
Try it yourself

Write a throttle that runs at most once per 300ms.

Track lastRun time, skip if too soon.
function throttle(fn, ms){ let last=0; return (...a)=>{ const now=Date.now(); if(now-last>=ms){ last=now; fn(...a); } }; }
23

Currying & Function Composition

20 min
What you'll learn
  • Transform multi-arg functions
  • Compose small functions
  • Write functional code

Currying converts a function taking multiple arguments into a chain of single-argument functions. Composition builds complex behavior by piping functions together: compose(f, g)(x) = f(g(x)). These patterns create tiny, reusable, testable pieces that snap together — the heart of functional programming, and increasingly common in modern frontend (Redux, React hooks, etc.).

curry.js
const curry = (fn) => (a) => (b) => fn(a, b);
const add = (a, b) => a + b;
const curriedAdd = curry(add);

const compose = (f, g) => (x) => f(g(x));
const double = x => x * 2;
const inc = x => x + 1;
const doubleThenInc = compose(inc, double);

console.log(curriedAdd(2)(3)); // 5
console.log(doubleThenInc(5)); // 11
Output
console
5 11
💡 Tip: Compose reads right-to-left: compose(f,g)(x) applies g first, then f — like math notation.
Try it yourself

Curry a multiply(a,b) function and use it to create a double function.

const double = curriedMultiply(2).
const mult = a => b => a*b;
const double = mult(2);
double(5); // 10
24

Memoization

16 min
What you'll learn
  • Cache function results
  • Avoid recomputation
  • Speed up expensive functions

Memoization caches a function's result for each set of arguments, so calling it again with the same inputs returns instantly. It's perfect for expensive pure functions — recursive Fibonacci, sorting, API calls with repeated queries. The trade-off is memory, so memoize only functions where the same inputs occur frequently.

memoize.js
function memoize(fn) {
  const cache = new Map();
  return (arg) => {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}
const slowSquare = memoize(n => { console.log('computing'); return n * n; });
slowSquare(4); slowSquare(4); // 'computing' logs once
Output
console
computing 16 16
⚠️ Common Mistake: Memoization only works for pure functions — same input must always produce same output.
Try it yourself

Memoize a factorial function.

Store results in an object keyed by n.
const cache = {};
function fact(n){ if(n in cache) return cache[n]; cache[n] = n<=1?1:n*fact(n-1); return cache[n]; }
25

Recursion & Tree Traversal

22 min
What you'll learn
  • Solve problems recursively
  • Traverse nested structures
  • Handle deeply nested data

Recursion is a function calling itself until a base case. It's the natural tool for nested data — JSON structures, file trees, comment threads. Tree traversal (visiting every node) is recursion's killer app. Once you can walk a tree, you can render comments, build menus, and process any hierarchical data with confidence.

recursion.js
function walk(node) {
  console.log(node.name);
  (node.children || []).forEach(walk);
}
const tree = {
  name: 'root',
  children: [
    { name: 'a', children: [{ name: 'a1' }] },
    { name: 'b' }
  ]
};
walk(tree);
Output
console
root a a1 b
🔎 Important: Always define a base case — forgetting it causes infinite recursion and a stack overflow.
Try it yourself

Write a recursive sum for nested number arrays.

If element is array, recurse; else add.
function sum(arr){ return arr.reduce((t,x)=>t+(Array.isArray(x)?sum(x):x),0); }
26

Regular Expressions

22 min
What you'll learn
  • Match patterns with regex
  • Validate inputs
  • Extract data from strings

Regular expressions describe text patterns — emails, phone numbers, dates, URLs. They're a language unto themselves, and while they look like line noise at first, they're unmatched for validation and text extraction. Combined with String methods like match, replace, and test, regex is one of the highest-leverage tools for real-world data handling.

regex.js
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailPattern.test('[email protected]')); // true
console.log(emailPattern.test('not-an-email')); // false

const text = 'Order #1234 confirmed';
console.log(text.match(/\d+/)[0]); // '1234'
Live Preview
browser — Regular Expressions
test@example.com → ✓ match
⚠️ Common Mistake: Regex for email is famously imperfect — for production validation, prefer a battle-tested library.
Try it yourself

Write a regex to check if a string is all digits.

/^\d+$/
const isDigits = /^\d+$/.test('12345'); // true
27

The Module Pattern & Encapsulation

16 min
What you'll learn
  • Create private state
  • Expose public APIs
  • Organize code without classes

The module pattern uses closures to create private variables and return a public API. It was THE way to organize JavaScript before ES modules, and the pattern still appears everywhere — in plugins, libraries, and legacy code. Understanding it means reading and maintaining a decade of JavaScript codebase with confidence.

module-pattern.js
const Counter = (function() {
  let count = 0;
  return {
    increment() { count++; return count; },
    get value() { return count; }
  };
})();
Counter.increment();
Counter.increment();
console.log(Counter.value); // 2
console.log(Counter.count); // undefined — private!
Live Preview
browser — The Module Pattern & Encapsulation
Outercount=0
Innercount++
✓ Best Practice: The IIFE runs once, creating a persistent private scope — count is truly hidden from the outside.
Try it yourself

Create a module with a private password and a public check() method.

IIFE closing over the password.
const Auth = (()=>{ const pw='secret'; return { check(p){ return p===pw; } }; })();
28

Advanced Error Handling & Custom Errors

18 min
What you'll learn
  • Create custom error classes
  • Classify error types
  • Build robust error boundaries

Real apps fail in specific, knowable ways — validation errors, network errors, auth errors. Custom error classes let you throw and catch typed errors, so your handlers can respond appropriately instead of a blanket catch. Combined with try/catch at boundaries, this turns cryptic failures into clear, recoverable states.

errors-advanced.js
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}
function validateAge(age) {
  if (age < 18) throw new ValidationError('Must be 18+');
  return true;
}
try {
  validateAge(15);
} catch (e) {
  if (e instanceof ValidationError) console.log('Validation:', e.message);
  else console.log('Unknown:', e);
}
Output
console
Validation: Must be 18+
🔎 Important: instanceof checks let you branch on error type instead of fragile string matching on message.
Try it yourself

Create a NetworkError class and catch it separately.

extends Error + instanceof check.
class NetworkError extends Error {}
try { throw new NetworkError('offline'); } catch(e){ if(e instanceof NetworkError) console.log('net'); }
29

Intersection Observer

20 min
What you'll learn
  • Detect element visibility
  • Build infinite scroll
  • Implement lazy loading

IntersectionObserver watches when elements enter or leave the viewport. It replaces scroll-position math with an efficient callback API — perfect for infinite scroll, lazy-loading images, scroll-triggered animations, and 'has the user seen this?' analytics. It's dramatically more performant than listening to scroll events.

observer.js
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('Visible:', entry.target.id);
    }
  });
}, { threshold: 0.5 });
observer.observe(document.querySelector('#section2'));
Live Preview
browser — Intersection Observer
65% visible — IntersectionObserver fired
✓ Best Practice: Threshold 0.5 fires when half the element is visible — tune it for your use case.
Try it yourself

Observe an element and log when it's fully visible (threshold 1).

threshold: 1 in the config.
new IntersectionObserver(cb, { threshold: 1 }).observe(el);
30

Mutation Observer

16 min
What you'll learn
  • Watch DOM changes
  • React to dynamic content
  • Build live-updating UI

MutationObserver watches the DOM and fires when nodes are added, removed, or changed. It's the tool for reacting to content you don't control — third-party widgets, dynamically injected markup, or observing your own renders. When you need to run code 'after something appears on the page', this is the clean way.

mutation.js
const observer = new MutationObserver((mutations) => {
  mutations.forEach(m => {
    console.log('Added nodes:', m.addedNodes.length);
  });
});
observer.observe(document.body, { childList: true, subtree: true });
Output
console
Added nodes: 1 Added nodes: 2
💡 Tip: Use subtree: true carefully — observing the whole document can get noisy and slow.
Try it yourself

Observe a container and log whenever a child is added.

childList: true on the container.
new MutationObserver(ms => ms.forEach(m => console.log(m.addedNodes.length))).observe(container, { childList: true });
31

Service Workers & Offline Support

25 min
What you'll learn
  • Register a service worker
  • Cache assets for offline
  • Understand PWA basics

Service workers run in the background, independent of the page, enabling offline support, background sync, and push notifications — the heart of Progressive Web Apps. They intercept network requests and serve cached responses. This is how apps like Twitter Lite work on flaky connections, and it's a career-defining skill for frontend developers.

sw.js
// Register (main.js)
navigator.serviceWorker.register('/sw.js');

// sw.js
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open('v1').then(cache =>
      cache.addAll(['/', '/styles.css', '/app.js'])
    )
  );
});
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then(res => res || fetch(e.request))
  );
});
Output
console
Assets cached on first load; on subsequent visits they load offline from the cache.
🔎 Important: Service workers require HTTPS (or localhost) — they won't work on insecure origins.
Try it yourself

Register a service worker at /sw.js.

navigator.serviceWorker.register('/sw.js').
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
32

Fetch Advanced: AbortController & Streaming

22 min
What you'll learn
  • Cancel in-flight requests
  • Handle large responses
  • Build robust data loading

AbortController lets you cancel fetch requests — essential when a user navigates away or types a new search. Streaming responses read data incrementally with response.body.getReader(), enabling progress bars and processing huge files without loading them all into memory. These two features turn simple fetch into production-grade data handling.

abort.js
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch('/api/slow', { signal: controller.signal });
  const data = await res.json();
  console.log(data);
} catch (e) {
  if (e.name === 'AbortError') console.log('Request cancelled');
} finally {
  clearTimeout(timeout);
}
Output
console
Request cancelled (if the request exceeds 5 seconds)
✓ Best Practice: Always abort fetches on unmount/timeout — it's the #1 fix for stale-data bugs in SPAs.
Try it yourself

Create an AbortController and abort after 2 seconds.

setTimeout(() => controller.abort(), 2000).
const c = new AbortController();
setTimeout(() => c.abort(), 2000);
fetch(url, { signal: c.signal });
33

WebSockets for Real-time Apps

25 min
What you'll learn
  • Establish a WebSocket connection
  • Send and receive messages
  • Build real-time features

WebSockets open a persistent two-way connection — the server can push messages without the client asking. This powers chat, live dashboards, multiplayer games, and stock tickers. Unlike polling (wasteful) or HTTP requests (one-way), WebSockets deliver true real-time with minimal overhead.

websocket.js
const socket = new WebSocket('wss://example.com/chat');

socket.addEventListener('open', () => {
  socket.send('Hello server!');
});
socket.addEventListener('message', (event) => {
  console.log('Received:', event.data);
});
socket.addEventListener('close', () => {
  console.log('Disconnected');
});
Live Preview
browser — WebSockets for Real-time Apps
Live connection open
🔎 Important: WebSocket URLs use ws:// or wss:// (secure) — not http(s).
Try it yourself

Connect to a WebSocket and log the open event.

new WebSocket(url), addEventListener('open', ...).
const ws = new WebSocket('wss://x.com');
ws.addEventListener('open', () => console.log('connected'));
34

Canvas API Basics

25 min
What you'll learn
  • Draw shapes and lines
  • Render text on canvas
  • Build data visualizations

Canvas gives you a pixel grid to draw on with JavaScript — shapes, lines, text, gradients, and images. It powers charts, games, image editors, and animations where the DOM is too slow. Unlike SVG (vector), Canvas is raster — perfect for thousands of moving objects, particle effects, and custom visualizations.

canvas.js
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#e11d48';
ctx.fillRect(10, 10, 100, 60);
ctx.beginPath();
ctx.arc(150, 40, 30, 0, Math.PI * 2);
ctx.fillStyle = '#6366f1';
ctx.fill();
Live Preview
browser — Canvas API Basics
💡 Tip: Always set canvas width/height via attributes (or JS), not CSS — CSS scaling distorts the drawing.
Try it yourself

Draw a green circle at (100, 100) with radius 40.

ctx.arc(100, 100, 40, 0, Math.PI*2).
ctx.fillStyle='green'; ctx.beginPath(); ctx.arc(100,100,40,0,Math.PI*2); ctx.fill();
35

Drag & Drop API

20 min
What you'll learn
  • Make elements draggable
  • Handle drop targets
  • Build sortable interfaces

Native drag-and-drop lets users grab elements and move them — useful for file uploads, kanban boards, and sortable lists. It's event-driven: dragstart on the source, dragover and drop on the target. While finicky, native DnD avoids pulling in a heavy library for simple use cases.

dragdrop.js
const item = document.querySelector('#item');
const zone = document.querySelector('#zone');

item.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', 'item');
});
zone.addEventListener('dragover', (e) => e.preventDefault());
zone.addEventListener('drop', (e) => {
  e.preventDefault();
  console.log('Dropped!');
});
Output
console
Dropped!
💡 Tip: dragover MUST call preventDefault() or the drop event never fires — the classic gotcha.
Try it yourself

Add a dragstart that sets some data.

e.dataTransfer.setData('text', 'value').
el.addEventListener('dragstart', e => e.dataTransfer.setData('text', 'hello'));
36

Custom Events

16 min
What you'll learn
  • Create and dispatch custom events
  • Decouple components
  • Build event-driven UIs

Custom events let different parts of your app communicate without direct coupling. Component A dispatches a 'userLoggedIn' event; components B and C listen and react independently. This loose coupling makes code more modular and testable — a pattern used heavily in framework design and large applications.

events-custom.js
const loginEvent = new CustomEvent('userLoggedIn', {
  detail: { name: 'Sufyan' }
});
document.addEventListener('userLoggedIn', (e) => {
  console.log('Welcome', e.detail.name);
});
document.dispatchEvent(loginEvent);
Output
console
Welcome Sufyan
✓ Best Practice: The detail property carries your payload — think of it as the event's arguments.
Try it yourself

Dispatch a 'cartUpdated' event with { count: 3 }.

new CustomEvent('cartUpdated', { detail: { count: 3 } }).
document.dispatchEvent(new CustomEvent('cartUpdated', { detail: { count: 3 } }));
37

Web Components (Custom Elements)

25 min
What you'll learn
  • Create custom HTML tags
  • Encapsulate with Shadow DOM
  • Build framework-free components

Web Components let you define your own HTML elements — <user-card>, <todo-item> — that work in any framework or no framework at all. Custom Elements plus Shadow DOM give you true encapsulation (styles don't leak in or out). They're the platform-native answer to React/Vue components, and browsers now support them natively.

web-component.js
class UserCard extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<div class="card">' + this.getAttribute('name') + '</div>';
  }
}
customElements.define('user-card', UserCard);
Output
console
<user-card name="Sufyan"></user-card> renders as a card showing 'Sufyan'.
🔎 Important: Custom element names MUST contain a hyphen — that's how the browser distinguishes them from built-ins.
Try it yourself

Define a custom <greeting-tag> element.

customElements.define('greeting-tag', class extends HTMLElement {...}).
class G extends HTMLElement { connectedCallback(){ this.textContent='Hello'; } }
customElements.define('greeting-tag', G);
38

State Management Patterns

22 min
What you'll learn
  • Understand UI state
  • Implement a simple store
  • Use pub/sub patterns

As apps grow, shared state gets messy — components need the same data but live far apart. A central store with subscribe/notify solves this: components read from the store and re-render when it changes. This is the core idea behind Redux, Vuex, and every state library — and you can build a basic version in 20 lines to truly understand it.

store.js
function createStore(initial) {
  let state = initial;
  const listeners = new Set();
  return {
    getState: () => state,
    setState: (next) => {
      state = next;
      listeners.forEach(fn => fn(state));
    },
    subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); }
  };
}
const store = createStore({ count: 0 });
store.subscribe(s => console.log('State:', s));
store.setState({ count: 1 });
Output
console
State: { count: 1 }
✓ Best Practice: The subscribe/notify pattern is the heart of reactive UIs — understand it and every framework makes sense.
Try it yourself

Add an unsubscribe function to the store.

Return a function that removes the listener.
subscribe(fn){ ls.add(fn); return () => ls.delete(fn); }
39

Code Splitting & Lazy Loading

22 min
What you'll learn
  • Split code into chunks
  • Load modules on demand
  • Reduce initial bundle size

Code splitting breaks your app into chunks that load only when needed. Dynamic import() returns a Promise that loads a module on demand — perfect for routes, heavy libraries, and features users might never open. The result: faster initial load, less JavaScript parsed, better performance scores. Every serious app ships with some form of this.

split.js
const button = document.querySelector('#loadChart');
button.addEventListener('click', async () => {
  const { drawChart } = await import('./chart.js');
  drawChart();
});
Output
console
The chart module downloads only when the button is clicked, keeping the initial page light.
✓ Best Practice: Dynamic import() is the simplest code-splitting tool — no build config required for the core idea.
Try it yourself

Dynamically import a module and call one of its exports.

const m = await import('./mod.js'); m.fn();
const m = await import('./util.js');
m.doSomething();
40

Capstone: Build a Complete SPA

50 min
What you'll learn
  • Combine all advanced skills
  • Build a single-page app
  • Ship a portfolio project

Your final project: a single-page app with client-side routing, a central store, lazy-loaded views, and real API data — no framework. This pulls together every advanced skill: modules, state management, async/await, error handling, observers, and performance. It's the kind of project that proves you're ready for real frontend work and belongs in your portfolio.

spa.js
// app structure
const routes = {
  '#/home': () => renderHome(),
  '#/users': async () => {
    const users = await fetch('/api/users').then(r => r.json());
    renderUsers(users);
  }
};
function router() {
  const hash = location.hash || '#/home';
  (routes[hash] || routes['#/home'])();
}
window.addEventListener('hashchange', router);
router();
Live Preview
browser — Capstone: Build a Complete SPA
Tasks
5
Views
1.2k
SPA Dashboard
✓ Best Practice: This one project demonstrates more than any tutorial — ship it, share it, and you're job-ready.
Try it yourself

Add a #/about route to the router.

routes['#/about'] = renderAbout.
routes['#/about'] = () => renderAbout();
You've completed all 40 advanced lessons. You're now a frontend engineer.

Practice your skills or return to the Web Development hub.

📱 Scan this QR code with your phone camera to instantly open this page.

Works on iOS, Android, and any modern device. No app installation required.

Account Verified!

Your email has been verified successfully.