feat: dual-mode JQuery/native DOM backend#493
feat: dual-mode JQuery/native DOM backend#493jankapunkt merged 6 commits intometeor:release-3.1.0from
Conversation
Detect jQuery at load time. If present, all existing jQuery code paths are used (zero breaking changes). If absent, native DOM APIs are used. A deprecation message is logged when jQuery is detected. Ref meteor#490
There was a problem hiding this comment.
Pull request overview
This PR implements a dual-mode DOM backend for Blaze, making jQuery optional rather than required. When jQuery is present, all existing code paths are preserved. When absent, native DOM APIs are used. This is a single-file change to packages/blaze/dombackend.js that replaces the hard dependency on jQuery with a runtime detection mechanism, addressing issue #490 and building on the direction started by PR #474.
Changes:
- jQuery detection at load time with a deprecation notice, replacing the hard crash when jQuery is absent
- Native DOM implementations for
parseHTML(using<template>element), event delegation (addEventListener+closest()), event capture, teardown callbacks, andfindBySelector(querySelectorAll) - Refactored jQuery special event registration and teardown callback execution into shared/conditional code paths
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix bindEventCapturer bug: check elem.contains(matched) instead of
elem.contains(target) to prevent firing when selector matches an
ancestor outside the container
- Add text node guard (nodeType check) in delegateEvents and
bindEventCapturer to prevent TypeError on text node targets
- Deduplicate getContext() — branch only on jQuery legacy support check
- Fix WeakMap comment: value is Array<{wrapper, eventType}>, not {wrapper, type}
- Fix handler key comment: eventType stored in entry, not used as key
|
@dupontbertrand the last task item "Unit tests with native backend (blocked on render_tests.js conditional — follow-up)" will be added here or in another PR? |
As you want, I think it's not that important right ? So I can do an other PR for that ? Tbh when I'm thinking about it now I don't see the added value since we keep JQ for the moment |
|
@dupontbertrand I will add the migration notes to my incoming PR for the new vitepress docs |
|
@jankapunkt Do I need to think about "new templates" for --create ? Or change existing one for removing JQuery by default in the next release ? |
|
@dupontbertrand sorry I don't understand what do you exactly mean? |
|
@jankapunkt Yes sorry : Shouldn’t the meteor create templates stop including jQuery by default? (--blaze and --full) |
|
Yes that's the skeleton that need to be updated but I wouldn't do that before we released 3.1 unless @italojs thinks otherwise |
|
@dupontbertrand I was close to merge this but I found, that tests currently only cover the jQuery implementation, however we should have tests in place that also test for the absence of jQuery not only for the sake of functionality but also for preventing regressions in the future. |
|
Omw boss 🧐🫡 |
|
I want to make sure I understand the expected testing scope correctly 😬 Right now, as long as tests load jQuery strongly, How do you see the right approach here?
Which direction would you prefer ? |
|
@dupontbertrand I think we should leverage Meteor.isTest or Meteor.isDevelopment to attach a Method that enables to switch to native mode. Let me try to draft something today. |
|
Uhm where does this coderabbitai come from? |
|
@nachocodoner 👀 ? |
|
We added it on Meteor project recently to expeirment on that AI tool, trying to make it less noisy in this PR, meteor/meteor#14271 What I can't understand is why it affects all meteor repos. I will talk with @fredmaiaarantes as he may activated for the whole organization. |
|
Thank you @dupontbertrand for your amazing efforts |
|
As for the open questions:
I'm no typescript dude so I'll defer it to those who know better @radekmie @perbergland @wreiske @ebroder
Seems like a great addition to be honest but definitely requires some work and coordination with @nachocodoner as he's already updating examples - meteor/examples#42
Actually I'd say a little changelog would suffice with very few examples. Because it's a straight forward. But maybe this is me.
I'd follow suite with what @jankapunkt's requests, as he's A) an old trusted contributor B) uses Blaze on an everyday basis and still cares about it |
|
For new Projects I think we do not need a --no-jquery flags but just update the package list in the skeleton, once 3.1.0 is out or am I missing something? |
|
@jankapunkt I think @harryadel meant suggesting examples both with and without jQuery ? |
|
@dupontbertrand I am merging this into the release-3.1.0 release and work on the tests there. |
|
@dupontbertrand please review the follow-up #497 |
Here's the jQuery PR. Since this is a critical change, I had Claude help draft a comprehensive write-up covering the approach, benchmarks, and open questions — it's a bit long but I wanted to make sure everything is laid out clearly for review.
Context
Issue #490 discusses removing jQuery (~90KB) from Blaze's
dombackend.js. As @jankapunkt noted, simply removing jQuery is a breaking change (instance.$()returns a plain Array instead of a jQuery object), unsuitable for a minor release.This PR implements a dual-mode backend: jQuery is detected at load time. If present, all existing code paths are preserved (zero breaking changes). If absent, native DOM APIs are used. A deprecation message is logged when jQuery is detected.
Relationship with #474
PR #474 by @harryadel started exploring this direction by tackling
parseHTMLreplacement. This PR builds on the same idea but extends it to the entire DOM backend:parseHTML(WIP, first step)dombackend.js(parseHTML, events, teardown, findBySelector)innerHTML+ regex wrapping +sanitize-html<template>element (3 lines, handles<tr>/<td>natively)sanitize-html(npm)Credit to @harryadel for initiating this effort and identifying the
parseHTMLpath.What changed
Single file modified:
packages/blaze/dombackend.js(169 additions, 90 deletions)All other Blaze files (
domrange.js,events.js,template.js,package.js) required no changes — they already go through theDOMBackendabstraction.$.parseHTML()<template>element +content.childNodes$.on(type, selector, handler)addEventListener+event.target.closest(selector)+ WeakMap cleanup$.event.fix()+$.is($.find())closest()+elem.contains()focus→focusin/blur→focusoutaliasing$.cleanData()→ jQuery special event_executeTeardownCallbacks()— no jQuery overhead$(selector, context)querySelectorAll()→ plain ArrayNo existing tests were modified (per @jankapunkt's request in #490).
How it works — technical details
Detection (lines 4-18)
Before, Blaze would crash immediately without jQuery (
throw new Error("jQuery not found")). Now:Every function in the file branches on
_hasJQuery. jQuery present → existing code path. jQuery absent → native path.parseHTML — why
<template>worksWhen Blaze renders HTML (e.g.
<div>{{name}}</div>), it needs to turn an HTML string into real DOM nodes.PR #474 tried to do this with a
<div>+ regex detection for table elements. The problem: a regular<div>has HTML parsing constraints. If you do:So #474 needed regex to detect
<tr>,<td>,<th>,<thead>, etc. and wrap them in the correct parent element before parsing.The
<template>element developer.mozilla.org is special in the HTML spec. Its.contentis aDocumentFragmentwith no parentage constraints — any HTML is valid inside it:The browser does all the work. No regex, no dependency, 3 lines:
Event delegation —
closest()+addEventListenerWhen you write:
Blaze doesn't attach a listener on every
.my-button. It attaches one listener on the parent container and checks if the click target matches the selector. This is "event delegation".With jQuery:
$jq(elem).on('click', '.my-button', handler)— jQuery handles it.Without jQuery:
closest(selector)walks fromevent.targetup through ancestors until it finds a match — exactly what jQuery does internally, but native.A
WeakMapstores the wrapper functions so they can be removed later inundelegateEvents.focus/blur aliasing
focusandblurdon't bubble — they don't propagate up the DOM, so delegation can't work with them. jQuery silently swaps them forfocusin/focusout(which do bubble). We do the same explicitly:Teardown — direct callback execution
When Blaze destroys a template (
Blaze.remove()), it runs cleanup callbacks (stopautoruns, release subscriptions, etc.).With jQuery, Blaze uses a clever hack: a "jQuery special event" (
blaze_teardown_watcher). When jQuery cleans up an element ($.cleanData), it triggers the teardown handler that runs the callbacks. This is indirect and carries$.cleanDataoverhead.Without jQuery, we call
_executeTeardownCallbacks(elem)directly on each element — no detour through jQuery internals. This is why benchmarks show -57% to -67% on teardown: we skip all of$.cleanData's overhead.findBySelector
The only area where jQuery is slightly faster (+12%) — its internal selector engine wraps results with less overhead than
Array.from(querySelectorAll()). In absolute terms: 1.7ms vs 1.9ms for 1,000 calls.Benchmarks
Tested on identical apps — only difference is presence/absence of the
jqueryMeteor package.Environment: Ubuntu 24.04, Chrome 145, Meteor 3.4, Node 22.13, ThinkPad P52 (i7-8850H, 64GB RAM)
DOM Operations (client-side, median of 5 runs)
Summary: Native is faster on render, parseHTML, events, and teardown (up to 67%). jQuery is slightly faster on
findBySelector, but the difference is negligible in absolute terms (1.7ms vs 1.9ms for 1,000 calls).Reactive / MongoDB (network-bound)
No significant difference — the bottleneck is DDP + MongoDB, not the DOM layer.
Bundle size
Removing jQuery saves ~90KB minified (~30KB gzipped) from the client bundle.
Event coverage
Manually tested 30+ DOM event types in both apps (with and without jQuery), including non-bubbling events (mouseenter, mouseleave, scroll, pointerenter, pointerleave) which go through Blaze's capture mode. All work correctly with the native backend.
Open questions for discussion
1. TypeScript definitions (
blaze.d.ts)Currently
blaze.d.tshasimport * as $ from 'jquery'and typesTemplateInstance.$()asJQuery<TElement>. Without jQuery, this returns a plainHTMLElement[]. Options:.d.tsfor each modeLooking for guidance on preferred approach.
2.
--no-jqueryflag formeteor createAll Blaze skeletons (
skel-blaze,skel-full,skel-legacy) currently include jQuery by default. A--no-jqueryflag or an updated skeleton would make it easier for new projects to opt into the native backend. This would be a separate PR onmeteor/meteor.3. Documentation
A migration guide would be needed:
meteor remove jquery)instance.$()returnsHTMLElement[]instead of jQuery object.$(calls that use jQuery-specific methods (.animate(),.fadeIn(), etc.)4. Existing test suite
All 416 existing Blaze unit tests pass with jQuery present. The test for
Array.isArray($found)atrender_tests.js:621would need a conditional check to also pass without jQuery — but per @jankapunkt's request, no tests were modified in this PR. Happy to add that in a follow-up.How to test this yourself
Test plan
instance.$()returns jQuery object (with jQuery) / plain Array (without)currentTargetcorrectly set on delegated events (verified with nested elements)Ref #490, relates to #474