Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

SwiftQ

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)

Install

CDN:

<script src="https://cdn.jsdelivr.net/npm/swiftq/swiftq.min.js"></script>

npm:

npm install swiftq

Usage - 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($) { /* ... */ });

Quick Start

// 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!') })

API Reference

Selectors

$('.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 ready

Any 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

Chain Navigation

// .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

Traversal

$('.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')

Filtering

$('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

DOM Manipulation

// 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, Properties & Data

// 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']

CSS & Classes

// 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

Dimensions & Position

$('.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.

Animations (Web Animations API)

// 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 transform and opacity when possible - they run on the compositor thread and never touch layout.

Performance & Scheduling ⚡

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()

Double Buffering & Morphing ⚡

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()

Element Creation

// $.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)

Templates & Rendering

// $.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

Conditional Classes

$.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

// $.portal() - move element to another location
$.portal('.modal', document.body)

Forms

$('form').serialize()         // → "name=John&email=john%40example.com"
$('form').serializeArray()    // → [{ name, value }, ...]

// Enable / disable
$('form button').disable()
$('form :input').enable()

Iteration

// .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()

Static Utilities

$.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>')              // → '&lt;b&gt;&quot;hi&quot;&lt;/b&gt;'
$.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

Plugins

$.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')

Why SwiftQ over jQuery/Zepto?

Security

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

Performance

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

Size

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()

Migration from jQuery

// 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).


Changelog

1.3.0

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 siblings
  • stop() reverted the element instead of freezing (now uses commitStyles)
  • stop() / superseding animate() produced unhandled promise rejections
  • delay().fadeIn() threw ReferenceError
  • $.template output corrupted by $ in data values; keys now regex-escaped
  • $.html didn't escape ' (single-quoted attribute injection); node interpolation only worked in the first root node
  • $.render with multiple containers filled only the first (fragment self-empties)
  • removeClass() with no args threw on SVG elements
  • $.each iterated length/selector as keys on collections
  • find() wasn't rooted (qSA quirk: ancestors outside the root could match)
  • Duplicate dead animate definition 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 with data-key support), .offline()
  • $.finder() memoized queries
  • Custom pseudo-classes: :input, :contains(), :selected, :visible, :hidden, :header, :parent, :animated + extensible $.pseudos registry
  • Rooted find() with leading-combinator support (find('> li'))
  • delay().animate() chaining

Performance

  • uniq/parents/add dedup: O(n²) → O(n) via Set
  • data() reads no longer allocate a store per element
  • Multi-node insertion: one native call per target instead of N

Browser Support

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).


License

MIT


SwiftQ - Fast DOM, less code, zero legacy, zero long tasks.

About

A modern DOM manipulation library with a performance scheduler

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages