A modern 9KB DOM manipulation library with a performance scheduler - the spiritual successor to jQuery & Zepto.
SwiftQ gives you the chainable $() syntax developers love, powered by 2026 browser APIs - plus something neither jQuery nor Zepto ever had: a built-in toolkit for staying under Google's 50ms Long Task limit. Time-sliced rendering, read/write batching, double-buffered re-renders, and minimal-diff DOM morphing.
jQuery 3.7 → 30.0 KB gzipped (events + ajax + everything)
Zepto 1.2 → 9.5 KB gzipped (events + ajax)
SwiftQ 1.3 → 9.2 KB gzipped (DOM + performance scheduler - no events, no ajax)
CDN:
<script src="https://cdn.jsdelivr.net/npm/swiftq/swiftq.min.js"></script>npm:
npm install swiftqUsage - all module formats supported:
// Browser global
<script src="swiftq.min.js"></script>
// → window.$ and window.SwiftQ available
// CommonJS
const $ = require('swiftq');
// ES Module (via bundler)
import $ from 'swiftq';
// AMD
define(['swiftq'], function($) { /* ... */ });// Select → chain → done
$('.card')
.addClass('visible')
.css({ transform: 'translateY(0)', opacity: 1 })
.find('.title')
.text('Hello SwiftQ')
.end() // ← back to .card
.show()
// Create elements without HTML strings
const card = $.create('div', { class: 'card', 'data-id': 42 }, [
$.create('h2', null, 'Title'),
$.create('p', { class: 'body' }, 'Content here')
])
$('#app').append(card)
// Animate with async/await
await $('.modal').fadeIn(300)
await $('.modal').fadeOut(300)
// Render a HUGE list without freezing the page (time-sliced)
await $.renderAsync('#users', users, user =>
$.create('li', { 'data-id': user.id }, user.name)
)
// Re-render a live component with ONE DOM operation (double buffer)
const $buf = $('#dialog').stage()
$buf.find('.title').html(newTitle) // free - detached clone
$buf.find('.footer').append(buttons)
$buf.commit({ morph: true }) // only real diffs touch the live DOM
// DOM Ready
$(function() { console.log('Ready!') })$('.class') // CSS selector
$('#id') // ID
$('div.active, .open') // multiple selectors
$('<div class="new">') // create from HTML
$(element) // wrap DOM element
$(nodeList) // wrap NodeList
$([el1, el2]) // wrap array
$(function() {}) // DOM readyAny standard CSS works - including modern selectors jQuery never had natively:
:has(), :is(), :where(), :not(complex, list), :focus-within, :nth-child(even).
Custom pseudo-classes (jQuery/Sizzle-compatible subset, applied as post-filters on top of the native engine):
$('form :input') // input, select, textarea, button
$('li:contains(שלום)') // text search (no CSS equivalent exists)
$('option:selected')
$(':header') // h1–h6
$('div:parent') // non-empty elements
$('.box:visible') // ⚠ reads layout per element - use on small sets
$('.box:hidden')
$('.box:animated') // currently animated by SwiftQ
// Register your own (like Sizzle's $.expr):
$.pseudos.external = el => el.host !== location.host
$('a:external').attr('rel', 'noopener')Custom pseudos are supported in the rightmost part of a selector
('form :input' ✓, ':input + label' ✗ throws a clear error).
Rooted find() - unlike raw querySelectorAll, $el.find('div p') only
matches when the whole pattern is inside the element (jQuery semantics).
Leading combinators work too:
$('#menu').find('> li') // direct children only// .end() - go back to previous collection
$('.list')
.find('.item').addClass('found')
.end() // back to .list
.addClass('searched')
// .addBack() - include previous set in current
$('.parent').children('.active').addBack()
// .pushStack(elements) - create sub-collection with chain link$('.list').find('.item') // descendants (rooted, pseudo-aware)
$('.item').closest('.container') // nearest ancestor
$('.item').parent() // direct parent
$('.item').parent('.wrapper') // filtered
$('.item').parents() // all ancestors
$('.item').parents('.section') // filtered
$('.item').children() // direct children
$('.item').children('.active') // filtered
$('.item').siblings() // siblings (excluding self)
$('.item').contents() // all child nodes incl. text
$('.item').next() // next sibling
$('.item').prev() // previous sibling
$('.item').nextAll() // all following siblings
$('.item').prevAll() // all preceding siblings
// Until methods - collect until selector matches
$('.deep').parentsUntil('.container')
$('.deep').parentsUntil('.container', '.section') // with filter
$('.start').nextUntil('.end')
$('.end').prevUntil('.start')$('li').filter('.active') // keep matching (custom pseudos OK)
$('li').filter(function(i, el) {}) // keep by function
$('li').not('.disabled') // exclude matching
$('li').has('.icon') // keep if descendant matches
$('li').is('.active') // test first element
$('li').eq(0) // element at index
$('li').first() // first element
$('li').last() // last element
$('li').slice(1, 3) // subset by range
$('li').add('.extra') // merge collections// Content
$('.box').html('<b>bold</b>') // set innerHTML
$('.box').html() // get innerHTML
$('.box').text('plain text') // set textContent
$('.box').text() // get textContent
$('.box').empty() // remove all children
// Inserting - multi-node order is preserved, single native call per target
$('.list').append('<li>a</li><li>b</li>') // → a, b
$('.list').prepend('<li>1</li><li>2</li>') // → 1, 2 (before existing)
$('.item').before('<hr>')
$('.item').after('<b>x</b><b>y</b>') // → x, y
// Reverse insertion
$('<li>new</li>').appendTo('.list')
$('<li>new</li>').prependTo('.list')
$('<li>new</li>').insertBefore('.item')
$('<li>new</li>').insertAfter('.item')
// Multi-target semantics match jQuery: clones for every target except
// the last - a single-target insert is a MOVE, not a copy.
// Removing & cloning
$('.item').remove()
$('.box').empty()
$('.template').clone() // deep clone
$('.template').clone(false) // shallow clone
// Wrapping
$('.item').wrap('<div class="w">')
$('.items').wrapAll('<section>')
$('.box').wrapInner('<div class="i">')
$('.item').unwrap()
// Replacing
$('.old').replaceWith('<div>new</div>')
$('<div>new</div>').replaceAll('.old')
// Swapping - works for adjacent siblings too
$('.item-a').swap('.item-b')
// Moving
$('.modal').appendTo('body') // moves element (not copies)// Attributes
$('.link').attr('href')
$('.link').attr('href', '/new')
$('.link').attr({ href: '/new', title: 'Link' })
$('.link').removeAttr('title')
// Toggle attributes (like toggleClass but for attrs)
$('details').toggleAttr('open')
$('input').toggleAttr('readonly', false)
// Read ALL attributes as object
$('#el').attrs() // → { id: 'el', class: 'box', 'data-x': '5' }
// Properties
$('input').prop('checked')
$('input').prop('checked', true)
$('input').removeProp('indeterminate')
// Values
$('input').val()
$('input').val('new text')
$('select[multiple]').val() // array of selected
// Data (WeakMap-backed - stores any type, auto-GC, zero allocation on read-miss)
$('.box').data('count', { x: 1 }) // store complex object
$('.box').data('count') // retrieve
$('.box').data() // get all data
$('.box').removeData('count')
// Pluck - extract property from each element
$('.user').pluck('id') // → ['user-1', 'user-2']
$('input').pluck('value') // → ['hello', 'world']
$('a').pluck('href') // → ['/home', '/about']// Reading CSS
$('.box').css('color')
$('.box').css(['color', 'font-size']) // → object
// Setting CSS
$('.box').css('color', 'red')
$('.box').css({
color: 'red',
fontSize: '14px', // camelCase
'margin-top': '10px', // dash-case
opacity: 0.5 // auto px where needed
})
// Classes (native classList - SVG-safe)
$('.box').hasClass('active')
$('.box').addClass('one two three') // multiple at once
$('.box').removeClass('old')
$('.box').removeClass() // remove all (works on SVG)
$('.box').toggleClass('open')
$('.box').toggleClass('open', true) // force add
$('.box').toggleClass('open', false) // force remove
// Show / Hide
$('.box').show()
$('.box').hide()
$('.box').toggle()
$('.box').toggle(true) // force show$('.box').width() // note: border-box, from BoundingClientRect
$('.box').height()
$('.box').width(200) // set
$('.box').innerWidth() // width + padding
$('.box').innerHeight()
$('.box').outerWidth() // width + padding + border
$('.box').outerWidth(true) // + margin
$('.box').outerHeight()
$('.box').outerHeight(true)
$('.box').offset() // { top, left, width, height }
$('.box').position() // { top, left } vs offset parent
$('.box').offsetParent()
$('.box').rect() // full getBoundingClientRect
$('.box').offsetTo('.container') // position relative to another element
$('.box').isVisible() // checks display + visibility + opacity
$('.box').isHidden()
$('.box').scrollTop()
$('.box').scrollTop(100)
$('.box').scrollLeft()
$('.target').scrollIntoView() // smooth scroll
$('.target').scrollIntoView(false) // instant⚠ Every method in this section reads layout. Inside loops, batch these reads with
$.measure()(below) to avoid layout thrashing.
// Basic - returns Promise. Cancelled/superseded animations resolve
// quietly (no unhandled rejections - ever).
await $('.box').animate({ opacity: 0, transform: 'scale(0.8)' }, 400)
await $('.box').animate({ left: '100px' }, 600, 'ease-in-out')
// Convenience
await $('.box').fadeIn(300)
await $('.box').fadeOut(300)
$('.box').slideDown(300)
$('.box').slideUp(300)
// Stop
$('.box').stop() // freeze at current position (commitStyles)
$('.box').stop(true) // jump to end values
// Delay - chainable thenable
await $('.box').delay(500).fadeOut(300)
await $('.box').delay(200).animate({ opacity: 1 }, 300)
// Reflow - force layout recalc between changes
$('.box').addClass('prepared').reflow().addClass('animate')Tip: animate
transformandopacitywhen possible - they run on the compositor thread and never touch layout.
The toolkit for staying under Google's 50ms Long Task limit (INP / TBT). None of this exists in jQuery or Zepto.
// ── $.renderAsync - time-sliced list rendering ──
// Renders in chunks, yielding the main thread between them. Scrolling and
// typing stay responsive even while inserting 10,000 rows.
await $.renderAsync('#list', bigArray, item => $.create('li', null, item.name))
await $.renderAsync('#list', items, fn, { chunk: 500, budget: 8 })
const ac = new AbortController()
$.renderAsync('#list', items, fn, { signal: ac.signal })
ac.abort() // cancel mid-render
// ── $.chunk - time-sliced processing of ANY array ──
// Yields on time budget (default 12ms) AND the moment the user interacts
// (navigator.scheduling.isInputPending) - near-zero input latency.
await $.chunk(rows, row => heavyWork(row))
await $.chunk(rows, fn, { budget: 8, signal: ac.signal })
// ── $.yield - hand the main thread back to the browser ──
// scheduler.yield() where available (prioritized continuation),
// MessageChannel fallback (works in background tabs - setTimeout doesn't).
for (const batch of batches) {
process(batch)
await $.yield()
}
// ── $.measure / $.mutate - layout-thrash-free scheduling ──
// All reads queued in a frame run BEFORE all writes (fastdom pattern).
// Interleaved read→write→read loops force a full reflow per iteration;
// this makes it one layout pass total.
$('.card').each(function () {
const el = $(this)
$.measure(() => el.height())
.then(h => $.mutate(() => el.css('min-height', h + 10)))
})
// ── $.batch - group writes into one frame ──
await $.batch(() => {
$('.header').addClass('sticky')
$('.sidebar').css('width', '250px')
})
// ── $.idle - non-urgent work when the thread is free ──
await $.idle(() => warmUpTemplates())
await $.idle(heavyFn, 2000) // but run within 2s at the latest
// ── $.nextFrame - requestAnimationFrame that cannot stall ──
// Real rAF in the foreground; MessageChannel in background tabs (where
// rAF is frozen and setTimeout is throttled to ~1s); safety-net timer for
// the switch-tabs-mid-render case. All SwiftQ scheduling uses this.
await $.nextFrame()For idempotent component re-renders (find-or-create, then dozens of
.html()/.attr()/.addClass() calls):
// ── .stage() / .commit() - edit a detached clone, swap once ──
const $buf = $('#dialog').stage() // back buffer (clone)
$buf.find('.title').html(newTitle) // free - no live DOM invalidation,
$buf.find('.footer').append(btns) // no MutationObserver wakeups
// ...async work is fine - the OLD content stays visible...
$buf.commit() // ONE live operation (focus preserved)
$buf.discard() // or throw the buffer away
// ── .commit({ morph: true }) - apply only the real diffs ──
// Unchanged nodes keep their identity: focus, scroll, CSS transitions,
// user-typed input values, and directly-bound listeners all survive.
// A re-render where 3 of 60 fields changed = exactly 3 DOM mutations.
$buf.commit({ morph: true })
// ── $.morph(live, next) - standalone minimal-diff update ──
$.morph('#list', newVersionElement)
// Give repeated siblings a stable data-key and reorders become MOVES:
// <li data-key="42">… - same node travels, state intact.
// ── .offline(fn) - detach, mutate freely, reattach ──
// A detached subtree has no layout: N writes cost 2 reflows total.
// (Synchronous only - for async re-renders use stage/commit.)
$('#big-table').offline((i, $el) => {
$el.find('td.price').each(/* hundreds of writes */)
})
// ── $.finder(root) - memoized find() for a render pass ──
const q = $.finder(surface)
q('.header').addClass('x') // queries once
q('.header').html(title) // cache hit - free
surface.prepend(tpl.header)
q.fresh('.header') // re-query after insertion
q.clear()// $.create(tag, attributes, children)
const list = $.create('ul', { class: 'menu', id: 'nav' }, [
$.create('li', null, 'Home'),
$.create('li', { class: 'active' }, 'About'),
$.create('li', null, [
$.create('a', { href: '/contact' }, 'Contact')
])
])
$('#app').append(list)
// With style object and conditional classes
$.create('div', {
class: $.classNames('alert', { 'alert-danger': isError }),
style: { padding: '10px', borderRadius: '4px' }
}, message)// $.template() - clone <template> with data filling
// HTML:
// <template id="card-tpl">
// <div class="card"><h2>{{title}}</h2><p>{{body}}</p></div>
// </template>
var card = $.template('#card-tpl', { title: 'Hello', body: 'World' })
$('#cards').append(card)
// Values are auto-escaped (XSS safe) - including quotes, and including
// values containing '$' (safe from regex replacement patterns).
// $.render() - render array into container (synchronous)
$.render('#user-list', users, function(user, i) {
return $.create('li', {
class: $.classNames('user', { first: i === 0 }),
'data-id': user.id
}, user.name)
})
// For big arrays use $.renderAsync (see Performance section).
// $.html`` - tagged template with auto-escaping
var name = '<script>alert("xss")</script>'
var el = $.html`<div class="card">${name}</div>`
// → XSS neutralized. Escapes < > & " AND ' (single-quoted attrs are safe).
// Interpolated DOM nodes work anywhere in the template.
// $.fragment() - batch DOM insertions
var frag = $.fragment()
for (var i = 0; i < 1000; i++) {
frag.append($.create('li', null, 'Item ' + i))
}
$('#list').append(frag) // 1 DOM insertion instead of 1000$.classNames('btn', 'btn-lg')
// → 'btn btn-lg'
$.classNames('btn', { active: true, hidden: false, open: isOpen })
// → 'btn active open'
$.classNames('base', null, undefined, 0, '', 'end')
// → 'base end'
$.classNames('a', ['b', 'c'], { d: true })
// → 'a b c d'// $.portal() - move element to another location
$.portal('.modal', document.body)$('form').serialize() // → "name=John&email=john%40example.com"
$('form').serializeArray() // → [{ name, value }, ...]
// Enable / disable
$('form button').disable()
$('form :input').enable()// .each()
$('li').each(function(index, element) {
console.log(index, $(this).text())
// return false to break
})
// .map()
var texts = $('li').map(function(i, el) {
return $(this).text()
})
// for...of (not in jQuery/Zepto!)
for (const el of $('li')) {
console.log(el.textContent)
}
// .toArray() - real Array
var elements = $('li').toArray()$.extend({}, defaults, options) // shallow merge
$.extend(true, {}, defaults, options) // deep merge
$.each(arrayOrObject, fn) // handles collections/NodeLists correctly
$.map(arrayOrObject, fn)
$.grep(array, filterFn)
$.inArray(item, array)
$.param({ a: 1, b: [2,3] }) // → "a=1&b[]=2&b[]=3"
$.contains(parent, child)
$.escapeHTML('<b>"hi"</b>') // → '<b>"hi"</b>'
$.isFunction(fn)
$.isArray(arr)
$.isNumeric(val)
$.isPlainObject(obj)
$.isWindow(obj)
$.isEmptyObject(obj)
$.parseJSON(str)
$.parseHTML('<div>safe</div>')
$.camelCase('font-size') // → 'fontSize'
$.trim(str)
$.noop$.fn.highlight = function(color) {
return this.css('background-color', color || 'yellow')
}
$('.important').highlight()
$('.error').highlight('#f00')
// Custom selector pseudos:
$.pseudos.empty_input = el => el.value === ''
$('form :input:empty_input').addClass('required')| Issue | jQuery/Zepto | SwiftQ |
|---|---|---|
| eval() on script insertion | Yes | Never |
| innerHTML for parsing | Yes | <template> (inert) |
| Built-in escaping helpers | No | $.html, $.template, $.escapeHTML |
| JSONP script injection | Zepto: yes | No AJAX module |
| Capability | jQuery/Zepto | SwiftQ |
|---|---|---|
| Time-sliced rendering (Long Task safe) | - | $.renderAsync, $.chunk |
| Input-aware yielding (isInputPending) | - | built-in |
| Read/write batching (anti-thrash) | - | $.measure / $.mutate |
| Double-buffered re-renders | - | .stage() / .commit() |
| Minimal-diff DOM updates | - | $.morph |
| Background-tab-safe scheduling | - | $.nextFrame, $.yield |
| Class manipulation | RegExp | Native classList |
| Data storage | Global object, never GC'd | WeakMap (auto GC) |
| Animation | CSS transitions + setTimeout | Web Animations API |
| Dedup / traversal | O(n²) indexOf | O(n) Set |
| Library | Gzipped | Events | AJAX |
|---|---|---|---|
| jQuery 3.7 | 30.0 KB | included | included |
| Zepto 1.2 | 9.5 KB | included | included |
| SwiftQ 1.3 | 9.2 KB | separate | use fetch() |
// These work exactly the same:
$('.items').find('.active').addClass('highlight').end().show()
$('form').serialize()
$('.box').css('color', 'red')
$('#el').data('key', value)
$('.template').clone().appendTo('#target')
$('form :input').disable()
$('li:contains(text)').remove()
// Events - use your event library:
// jQuery: $('.btn').on('click', handler)
// SwiftQ: ev.dom('.btn', 'click', handler)
// AJAX - use native fetch:
// jQuery: $.getJSON('/api/data', callback)
// SwiftQ: const data = await fetch('/api/data').then(r => r.json())
// Animations return Promises now:
// jQuery: $('.box').animate({opacity:0}, 300, callback)
// SwiftQ: await $('.box').animate({opacity:0}, 300)
// Positional selectors (deprecated in jQuery itself) → methods:
// jQuery: $('li:eq(2)'), $('li:first')
// SwiftQ: $('li').eq(2), $('li').first()Known behavioral differences: width()/height() return border-box values
(from getBoundingClientRect, affected by CSS transforms), and show() sets
display: block on elements hidden by a stylesheet (jQuery restores the
element's default display).
Fixed
prepend()/after()reversed the order of multiple inserted nodes- Multi-target insertion cloned for all targets, orphaning the originals (now: jQuery move semantics)
swap()silently failed for adjacent siblingsstop()reverted the element instead of freezing (now usescommitStyles)stop()/ supersedinganimate()produced unhandled promise rejectionsdelay().fadeIn()threwReferenceError$.templateoutput corrupted by$in data values; keys now regex-escaped$.htmldidn't escape'(single-quoted attribute injection); node interpolation only worked in the first root node$.renderwith multiple containers filled only the first (fragment self-empties)removeClass()with no args threw on SVG elements$.eachiteratedlength/selectoras keys on collectionsfind()wasn't rooted (qSA quirk: ancestors outside the root could match)- Duplicate dead
animatedefinition removed
Added
- Performance scheduler:
$.renderAsync,$.chunk,$.yield,$.measure/$.mutate,$.idle,$.nextFrame- all resilient to background tabs and missing rAF - Double buffering:
.stage()/.commit()/.discard(),$.morph(minimal-diff withdata-keysupport),.offline() $.finder()memoized queries- Custom pseudo-classes:
:input,:contains(),:selected,:visible,:hidden,:header,:parent,:animated+ extensible$.pseudosregistry - Rooted
find()with leading-combinator support (find('> li')) delay().animate()chaining
Performance
uniq/parents/adddedup: O(n²) → O(n) via Setdata()reads no longer allocate a store per element- Multi-node insertion: one native call per target instead of N
All modern browsers: Chrome, Firefox, Safari, Edge.
No IE - intentional. Core APIs (classList, matches(), closest(), WeakMap,
Web Animations, <template>, :scope) are standard since 2015+.
Progressive enhancement, feature-detected at runtime:
scheduler.yield() (Chrome 129+), navigator.scheduling.isInputPending()
(Chromium), Animation.commitStyles(), requestIdleCallback. Everything
degrades gracefully - including environments with no requestAnimationFrame
at all (workers, server-side loading).
MIT
SwiftQ - Fast DOM, less code, zero legacy, zero long tasks.