Why jQuery's .on() Replaced live() and delegate(): A Practical Look at Event Delegation
jQuery's .on() unified event delegation, replacing live() and delegate(). Learn how it works, when to use it, and the trade-offs around delegation roots, non-bubbling events, and the once: true option.
16 Mar 2026, 16:53 UTC

The Problem: Handlers on Every Element Don't Scale
You're building a dashboard with a table that loads rows via AJAX. Each row has a delete button. The naive approach — $('.delete-btn').click(handler) — works for the initial render but breaks as soon as new rows arrive. You could rebind after every fetch, but that's fragile and leaks memory when rows are removed.
Event delegation solves this by attaching a single listener to a stable ancestor and filtering at dispatch time. jQuery's .on() method (introduced in 1.7) unified this pattern, replacing the deprecated .live() and .delegate() APIs. Understanding how it works under the hood helps you avoid subtle bugs and make better performance choices.
How .on() Delegation Works Internally
When you call $(container).on('click', '.dynamic-button', handler), jQuery:
- Attaches one native
clicklistener tocontainer(which must exist at bind time). - Stores the selector
.dynamic-buttonand handler in its internal event registry. - When a click bubbles up to
container, jQuery runs the selector againstevent.target(and its ancestors) using Sizzle or nativematchesSelector. - If the selector matches, your handler runs with
thisset to the matched element.
The selector is evaluated at event time, not bind time. This means elements added later automatically work — but also means DOM mutations between bind and fire can change what matches.
Worked Example: Delegated Table Actions
Consider a table where rows are added dynamically:
<table id="orders">
<thead><tr><th>ID</th><th>Action</th></tr></thead>
<tbody><!-- rows injected here --></tbody>
</table>Bind once on the stable tbody:
const $tbody = $('#orders tbody');
$tbody.on('click', '.cancel-order', function(e) {
const orderId = $(this).closest('tr').data('order-id');
cancelOrder(orderId).then(() => $(this).closest('tr').remove());
});New rows appended to tbody — whether via .append(), a templating library, or framework interop — immediately respond to clicks. No rebind needed.
Trade-offs and Limitations
Delegation Root Choice Matters
Using document or body as the delegation root works but forces selector evaluation on every matching event across the page. For a high-frequency event like mousemove or a common selector like a, this adds measurable overhead. Prefer the nearest stable ancestor — tbody in the example above.
Non-Bubbling Events Need Alternatives
focus, blur, mouseenter, mouseleave do not bubble. Delegated .on('focus', 'input', fn) will never fire for descendant inputs. Use focusin/focusout instead (which do bubble):
// Won't work for dynamically added inputs
$form.on('focus', 'input', handler);
// Works
$form.on('focusin', 'input', handler);The once: true Option Is Per-Match, Not Global
Since jQuery 3.5, {once: true} removes the handler after the first invocation for each element-match pair. Clicking two different .btn elements fires both once; clicking the same element twice fires only the first time. This surprises developers expecting a single global fire.
Memory Profile: Direct vs. Delegated
A quick DevTools check: create 1,000 buttons with $('button').click(fn) vs. one $(container).on('click', 'button', fn). The direct approach allocates 1,000 handler closures; the delegated approach allocates one. Heap snapshots confirm the difference — useful when building long-lived SPAs with frequent DOM churn.
When to Consider Native Delegation
jQuery's event normalization (cross-browser event.target, event.preventDefault(), custom events) adds overhead. In performance-critical loops — e.g., a virtualized list with thousands of rows — native delegation with element.matches() can be faster:
container.addEventListener('click', e => {
const btn = e.target.closest('.dynamic-button');
if (btn) handler.call(btn, e);
});This avoids jQuery's internal registry lookup and selector re-evaluation per event. Profile first; the difference is often negligible for typical CRUD apps.
Actionable Checklist
- Use
.on(event, selector, handler)for any dynamic content — it's the supported, stable API since jQuery 1.7. - Bind to the nearest stable ancestor that exists at page load (not
documentby default). - For
focus/blur, switch tofocusin/focusoutin delegated handlers. - Test
once: truebehavior with multiple matching elements before relying on it. - If you're on jQuery 3.x slim,
.on()is still included — the event alias module (.click(),.hover(), etc.) is what's excluded.
Event delegation isn't a jQuery invention, but .on() made it the default, ergonomic choice. The API has been stable for over a decade; the main risks come from misunderstanding selector evaluation timing and event bubbling quirks — both avoidable with the patterns above.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.