Qbix/Q.js

All-In-One Front-End Web Framework from Qbix, alternative to jQuery, Angular, Vue, Ember etc.

JavaScript

31

141 commits

updated Sep 25, 2026

See the code

README

Q.js

All-In-One Front-End Web Framework from Qbix, alternative to jQuery, Angular, Vue etc.

Size: ~40KB (Minified + GZipped), compare to other frameworks

How to use: copy contents of dist into your project, and then include it like this:

File TypeCode to Use
.html files<script type="module" src="https://unpkg.com/@qbix/q/dist/Q.min.js">
.js or .ts filesimport Q from 'https://unpkg.com/@qbix/q/dist/Q.min.js';
DescriptionFull documentation here: https://qbix.com/platform/guide/javascript

This is part of the much larger full-stack Qbix Platform that contains many pre-built reusable tools, plugins, and requires PHP and Node.js on the back-end. If you want to build an entire full-stack social network like Facebook you're well-advised to go with that. But if you just want to use the lightweight front-end core, with your own back-end and other frameworks, then start with this framework here.

🌟 Advantages of Q.js vs other frameworks

1. No build step
React/Vue/Angular need bundlers, transpilers, tree-shakers, and hydration layers. Q.js works with plain .html and .js — drop it in and it runs.

2. Tiny footprint (~40KB gzipped)
Q.minimal.js is smaller than React (even without ReactDOM), Vue runtime, and far smaller than Angular. Yet it packs components, events, routing, caching, batching, i18n, animations, IndexedDB, service workers, and more — all in core.

3. Direct DOM updates (no Virtual DOM overhead)
No diffing, no reconciler. Q.js uses requestAnimationFrame and .rendering() hooks for high-performance, granular updates.

4. Autoloading methods & tools
Any method, template, or tool can be defined in its own file and is only loaded when first used. No code-splitting configs, no manual lazy-loading hacks — it’s automatic.

5. Tools = components + behaviors
Like React components or Vue directives, but attachable as behaviors to any DOM element. Multiple tools can live on the same element, making composition more flexible than “one component per node.”

6. Built-in batching & caching
APIs like Q.getter() and Q.batcher() deduplicate, throttle, and combine calls automatically. If multiple parts of the UI request the same object, it’s fetched once and everyone gets the result.

7. Templates that Just Work™
Use Handlebars, .html <template> files (Vue-style), or JS-defined templates. Designers can work in HTML, devs can work in JS — both paths integrate seamlessly.

8. First-class events & lifecycle
Events are observable, chainable (a.and(b), a.or(b), a.until(b)), and auto-clean up when a tool or page unloads. No more memory leaks or dangling listeners.

9. Progressive enhancement & SEO-friendly
Pages can render server-side HTML and Q.js simply activates tools as needed. No “blank page until hydration” problem — works with or without JS.

10. Unified, full-stack philosophy
It’s not just a front-end library. Q.js is part of the larger Qbix platform, which powers real social apps (with accounts, feeds, groups, payments, etc.). You can start small with Q.minimal.js, and later plug into the full stack without rewriting.

Q.js is smaller than jQuery, faster than React, easier than Vue, and more complete than Angular.

🔍 Features

Despite its size, Q.js implements many features not found in other front-end frameworks. Here is an overview of the main ones:

Class NameDescription
Q.Toolreusable components, activate with Q.activate(element)
Q.PageHTML pages, for your SPA, routes, browser history
Q.Eventevents and handlers, loaded and unloaded automatically
Q.Templatefor rendering templates, integrates with Handlebars
Q.Textfor loading internationalized translations and text for templates
Q.Methoddefines methods loading JS files asynchronously as needed
Q.Visualmanaging the front end interface, standard hints, as well as Q.Mask
Q.Animationfor animating using native Javascript animation
Q.Audiospeaking, loading and playing audio, etc.
Q.IndexedDBfor easy interaction with the built-in IndexedDB
Q.ServiceWorkerto manage service workers in a standard way
RequestsQ.request(), Q.handle(), Q.loadUrl(), Q.addScript(), Q.addStylesheet()
FlowQ.chain(), Q.getter(), Q.batcher(), Q.promisify(), Q.debounce()
HelpersQ.find(), Q.activate(), Q.cookie(), Q.handle()

Unlike JSX, HTMX or AlpineJS, your HTML markup stays standards-compliant, but now can look like:

<q-each person="@{page.people}" key="person.id">
  <p>${index}: @{person.name}</p>

  <q-if test="@{person.vip}">
    <strong>VIP</strong>
    <q-else-if person-age=">= 18">
      <span>Adult</span>
    </q-else-if>
    <q-else>
      <span>Under 18</span>
    </q-else>
  </q-if>
</q-each>

Overview

Main functionality

// Core
Q
  .init(options)
  .activate(element, options)
  .replace(element, source)
  .extend(target, ...sources)
  .copy(object, fields, levels)
  .exports(fn)
  .require(src, callback)
  .import(src)
  .handle(eventOrURL, context, args)

// Events
Q.Event
  .add(handler, key)
  .set(handler, key)
  .remove(key)
  .removeAllHandlers()
  .factory(name)
  .and(otherEvent)
  .or(otherEvent)
  .until(otherEvent)
  .then()
  .filter(testFn)
  .map(transformFn)
  .debounce(ms)
  .throttle(ms)
  .queue(ms)

// Tools
Q.Tool
  .define(name, constructor, defaults, methods)
  .prepare(elementOrTag, toolName, options)
  .remove(element)
  .clear(container)
  .onActivate(toolName)

// Pages
Q.Page
  .onLoad(urlOrName)
  .onUnload(urlOrName)
  .push(url, options)
  .pop()
  .currentUrl

// Templates
Q.Template
  .set(name, template, options)
  .remove(name)
  .compile(source, type, options)
  .render(name, fields, callback, options)
  .load(name, callback, options)

// Text / i18n
Q.Text
  .setLanguage(lang, locale)
  .set(name, data, options)
  .get(name, callback, options)
  .addFor(events, prefix, names)
  .addedFor(event, name, options)

// Methods
Q.Method
  .define(methods, namespace, closureFn)
  .options

// Visual
Q.Visual
  .intersection(a, b)
  .scrollLeft()
  .scrollTop()
  .windowWidth()
  .windowHeight()
  .boundingRect(element, classFilter, includeHidden)
  .getX(event, index)
  .getY(event, index)
  .shouldOpenInNewWindow(event)

// Animation
Q.Animation
  .play(fn, options)
  .pause()
  .jump(timeOrFrame)
  .rewind()
  .nextFrame()

// Audio
Q.Audio
  .play(src, options)
  .pause(id)
  .stop(id)
  .speak(text, options)

// IndexedDB
Q.IndexedDB
  .open(name, version, upgradeCallback)
  .get(store, key)
  .put(store, value)
  .remove(store, key)
  .clear(store)

// Service Workers
Q.ServiceWorker
  .register(script, options)
  .unregister(scope)
  .message(handler)

// Requests
Q.request(url, slotName, callback, options)
Q.loadUrl(url, options)
Q.addScript(src, callback, options)
Q.addStylesheet(href, options)
Q.removeStylesheet(href)
Q.cookie(name, value, options)

// Flow
Q.chain(tasks, callback)
Q.getter(original, options)
Q.batcher(batchFn, options)
Q.promisify(fn, useThis, callbackIndex)
Q.debounce(fn, ms)

📄 Pages

The concept of Pages is tied to HTTP resources and URLs in browsers.

Q.Page class can be used to manage pages and browser page history, loading and unloading their contents, stylesheets, etc. This happens automatically when you call Q.handle(url) or Q.loadUrl(url). The server-side can send a JSON payload with keys like scripts, stylesheets, etc. and the framework will call Q.addScript() and Q.addStylesheet() to add any new scripts and stylesheets, and Q.removeStylesheet() for any stylesheets that are not in the new page.

Pages can be divided into named slots (e.g. "navigation", "content") so that only parts of a page are requested from the server. The server typically responds with JSON containing slots, which is a map of {slotName: html} pairs, containing new HTML content. Then Q.replace() is used on slots that should be replaced, and finally Q.activate() is called to activate any new tools.

Here is how you add code to run when a page loads and before it unloads:

Q.page('Namespace/action', function () {
  // runs when page is loaded
  return function () {
    // runs before page is unloaded
  };
});

Q.Page.onLoad and Q.Page.onUnload are events that occur when pages are loaded an unloaded.

Q.Page.push(), Q.Page.pop() and Q.Page.currentUrl works with browser history.

More information: https://qbix.com/platform/guide/pages

📝 Templates

You can dynamically create elements in a React-like way, such as this:

Q.element('div', {id: "foo", "class": "bar baz"}, [
   Q.element('img', {src: "foo.png"}),
   Q.element('img', {src: "bar.png"})
]); 

Instead, you can define Handlebars templates like this:

Q.Template.set("Namespace/some/name", `put your template here`);

And then render them later like this:

Q.Template.render("Namespace/some/name").then(html => Q.replace(element, html));

Rendering templates that haven't been set yet causes requests to autoload from inside Q.Template.load.options.dir

🛠️ Tools

Tools represent re-usable components in Q.js -- just like in other front-end libraries, except Tools are only one part of a unified framework!

Here's how to define new types of Tools. Normally you'd define each one in its own file:

File: Namespace/js/tools/cool/name.js:

Q.Tool.define("Namespace/cool/name", constructor, defaultOptions, methods);

File: Namespace/js/tools/another.js:

Q.Tool.define("Namespace/another", function (options) {
  this.refresh(); // call method of tool
}, {
  x: 1,
  y: 2
}, {
  refresh: function () {
    this.state; // copy of options
    this.element; // the element it was activated on
    this.renderTemplate('Namespace/another/view', this.state,
      function (html, elements, tools) {
        // now this.x and this.y point to elements from
        // the template that was rendered, while
        this.element.forEachTool('Namespace/cool/name', function () {
            // run whenever a child tool activates
        });
      }, {
        some: options,
        'Namespace_cool_name_tool': {
            some: childToolOptions
        }
    });
    // this is how we handle in-place updates if x or y changes:
    this.rendering([x, y], (changed, previous, timestamp) => {
      Q.replace(this.elements.x, x); // very quick
      Q.replace(this.elements.y, y); // very quick
      this.element.addClass('updated_flash'); // some CSS effect
    });
    // to trigger these, anyone can simply call tool.stateChanged('x')
  },
  Q: {
    onInit: function () {
      // all child tools have been initialized
    }
    beforeRemove: function () {
      // cleanup, but see Events section!
    }
  }
});

// define a template with a child tool
Q.Template.set("Namespace/another/view",
   `<span class="Namespace_another_x">{{x}}</span>
    <span class="Namespace_another_y">{{y}}</span>
    {{{tool "Namespace/cool/name" "some-child-id" x=x y=3 z="foo"}}}`,
   {
      "elements": {
         "x": ".Namespace_another_x",
         "y": ".Namespace_another_y"
      }
   }
);

Optionally, you can also define tools in HTML files, Vue-style, which may result in nicer syntax highlighting for both the HTML templates and the Javascript:

<template id="Namespace/another/view">
   <span class="Namespace_another_x">{{x}}</span>
</template>
<script>
Q.Tool.define("Namespace/another/x", constructor, defaultOptions, methods);
// you can Q.Template.render("Namespace/another/view") as above
</script>

How to include tools in HTML:

<div class="Q_tool Namespace_tool_name Namespace-another-tool"
  data-namespace-cool-name='{
    "some": { "options": "go"}, "here": 2
  }'
  data-namespace-another='{
    "some": [ "other", options", here" ]
  }'
>
   <p>optionally might give the tool initial content</p>
</div>

How to prepare tools in JS:

// new component:
Q.Tool.prepare('div', toolName, options); // new div
// or add behaviors to existing element:
Q.Tool.prepare(element, toolName, options);
Q.Tool.prepare(element, anotherTool, otherOptions);

Multiple Tools can be defined on the same element. The Q.activate() function uses Q.find() to recursively find all elements with Q_tool CSS class, and then activates the tools in the order they've been defined. Tools are activated parent->child->grandchild, and then initialized grandchild->child->parent. You can pass options at activation-time, too, targeting tools by .classname or #id:

Q.activate(element, {
  '#specific_id': {
    "override": { "some": "options" },
  },
  '.Namespace_another_tool': {
     "some": ["more", "options" ]
  }
}); // finds and activates tools, then initializes them

Whenever you call Q.activate(document.body) it will traverse the whole document body, so it's slightly faster to call Q.activate() on containers where you've recently replaced HTML, rather than the whole document body.

You can remove tools manually before removing elements:

Q.Tool.clear(container); // child elements only
Q.Tool.remove(element); // also on element itself

but you usually don't need to, because Q.replace(element, source) does this for you automatically. The source can be an element, document fragment, or some HTML string.

Some tools might have data-q-retain attributes, causing them to be retained, and not removed and re-activated, if the incoming HTML has the same tool IDs. When a tool is retained, its tool.Q.onRetain event is triggered. However, incoming tools with a data-q-replace attributes replace even tools that had data-q-retain set.

Q.js even overrides $.fn.html() and $.fn.activate() in libraries like jQuery and $cash so that you can call the following to automatically cause old tools to be removed and new tools to be activated inside the element:

$(element).html(html).activate();

When tools are removed, all associated event handlers are removede automaitically! (See Events section below).

More information: https://qbix.com/platform/guide/tools

🧩 Components in HTML

Set Q.Tool.define.components = true before registering tools to give them custom element tags. A tool named Streams/chat becomes <streams-chat>. Attributes become tool options: publisher-id becomes publisherId, and on-custom-event can attach a handler when onCustomEvent is a Q.Event in the tool’s defaults.

The Q/if and Q/each tools let you write conditions, loops, and bindings directly in HTML:

<template id="People/vip">
  <strong>{{person.name}}</strong> has @{person.score} points.
</template>

<q-if test="true">
  <h2>@{page.title}</h2>

  <q-each person="@{page.people}" key="person.id">
    <article title="@{person.name}">
      <h3>${index}: @{person.name}</h3>

      <q-if person-age=">= 18" person-status="'active'">
        <p>Active adult</p>
        <q-else-if test="@{person.vip || person.score >= page.minimum}">
          <p>Special access</p>
        </q-else-if>
        <q-else><p>Waiting for access</p></q-else>
      </q-if>

      <q-if test="@{person.vip}" template="People/vip">
        <q-else><p>Standard member</p></q-else>
      </q-if>
    </article>
  </q-each>
</q-if>

⏰ Events

Add onFoo: new Q.Event() as properties on any object. By convention, the properties are called onFoo, and optionally beforeFoo (before the event occurs).

To trigger an event, call Q.handle(event, context, arguments). This will call all the handlers set on the event. If any of the handlers returns false then all subsequent handlers are skipped and Q.handle returns false. While being handled, event.occurring = true, and afterwards, event.occured = true.

Use onFoo.set(handler, key) to set a handler on an event, or onFoo.add(handler, key) to set it but also call it, if the event already .occurred === true.

You can call onFoo.remove(key) to remove handlers previously set for that key. Or call onFoo.removeAllHandlers() to remove all handlers for that event.

Authors of new Tools and Pages use Q.Tool.define(name, constructor) and Q.page(name, constructor). During the lifetime of a tool or page, outside code may add handlers to some events associated with those tools or pages. Normally, they'd need to follow it up with code to manually remove those events when the tool is removed or page is unloaded, i.e. during tool.Q.beforeRemove and Q.Page.beforeUnload events, respectively.

However, Q.js has a great way to automate the removal of events. Simply pass the tool or true instead of the key, as follows:

Call TypeDescription
event.set(handler, string)need to manually call event.remove(key)
event.set(handler, tool)automatically removed when tool is removed
event.set(handler, true)automatically removed when current page is unloaded
$cash.on(event, tool, handler)automatically calls .off() when tool is removed
$cash.on(event, true, handler)automatically removed when current page is unloaded

Calling set() or add() again with the same String key replaces previous handlers set with that same key. But if the key is not a String (i.e. it's a Q.Tool or true) then the handlers are added to the existing ones, while event.remove(tool) event.remove(true) removes everything added for that tool or current page.

You can define your own event factory very easily using the Q.Event.factory() function. The Event Factory pattern is used in order to create events on demand. For example, here we call an event factory to produce an event and add a handler to be run whenever a tool is activated

Q.Tool.onActivate(toolType) // retrieves or creates new Q.Event
   .add(handler, "MyModule"); // adds the handler with a key

Q.Event also supports Observable Streams / Reactive Events, through methods like

methodthe new event
a.and(b)occurs when both occurred`
a.or(b)occurs when either occurred
a.stop()indicates event won't be occurring anymore
a.until(b)occurs when a occurs, until b starts occurring
a.then()occurs only after a stops occurring
a.filter(test)occurrs only if test returns true
a.map(transform)occurrs after transforming parameters
a.debounce(ms)occurs only during a pause in a being fired
a.throttle(ms)occurs when a occurs, but at most once every ms
a.queue(ms)queues up occurrences to happen every ms

More information: https://qbix.com/platform/guide/eventsClient

⚙️ Methods

There are multiple ways to autoload external tools on demand.

// Analogues of node.js modules
Q.exports(function () { ... }); // in a file
Q.require(src, callback); // loading the file
Q.import(src).then(...); // wrapper around native import

But also, any asynchronous methods that utilize a callback or promise can be autoloaded:

Q.Data = Q.Method.define({
  all: function (a, b) {
    // regular method
  },
  digest: new Q.Method(),
  compress: new Q.Method(),
  decompress: new Q.Method(),
  sign: new Q.Method(),
  verify: new Q.Method(),
}, "Q/Data", function() {
  // pass variables in a closure
  return [Q, something];
}));

and then you can define the methods in files like Q/Data/digest.js:

Q.exports(function (Q, something) { // receive closures from main file
    return function Q_Data_digest(algorithm, payload, callback) {
        // here you have access to both the parameters and the closures!!
        return doStuff.then(function (result) {
            callback && callback(null, result); // callback interface
            return result; // promise interface
        });
    };
});

You should consider using this extensively, to organize your front-end code and load only as needed:

Q.Data.sign(algorithm, payload) // autoload method's code on demand
.then(...) // continue after promise resolves

📂 Objects

Use Q.copy(object, fields, levels) to copy an object. Sub-objects may expose custom .copy() methods to be used.

Do Q.extend(target, levels, obj1, obj2) method is used to modify target, adding from extension.

Do result = Q.extend({}, defaults, levels, obj1, obj2) to overrides some defaults with specific values.

You can extend Q.Event objects, Q.extend(target.onFoo, {"bar": handler)) will call target.onFoo.set(handler, bar). This allows you to pass custom event handlers that will be added to a copy of an event:

Q.Tool.define("Foo/bar", function (options) {
   this.state // this is Q.extend({}, defaults, options);
}, {
  foo: ['a', 'b'],
  bar: ['a', 'b'],
  onFoo: new Event(function () {
     // default handler here
  }, "Foo/bar"), // with sensible key name
  childToolOptions: {
    onBaz: new Event() // empty event
  }
});

Every instance of the Foo/bar tool actually makes a copy of the default options, and then extends them, saving the result in tool.state, ready to use. So suppose you instantiated the tool as follows:

const element = Q.Tool.prepare('div', 'Foo/bar', {
   // override some options when making element
   foo: ['c', 'd'],
   bar: {replace: ['c', 'd']},
   onFoo: {
     "Foo/bar": function () {
       // override default handler, but only for this instance
     }
   }
});
document.body.appendChild(element);
Q.activate(element, {
  // override more options at activation time
  childToolOptions: {
    onBaz: {
      "myCoolPage": function () {
         // add this handler, but only for this instance
      }
    }
  }
});

Other tool instances won't have these handlers added. They'll have a copy of the default options, including the event objects, that wasn't extended with these.

Also notice what Q.extend() does with arrays. The foo above would become ['a', 'b', 'c', 'd'] while the bar would become ['c', 'd'], because the array was being replaced by an object with a key "replace", so it replaces the array with the given value (another array).

➕ Functions

By convention, methods that take options have default options defined in the options property on the method itself. When called, a copy of the default options extended with any options passed to the method. Example:

A.method = function (a, b, options) {
   const o = Q.extend({}, A.method.options, 10, options);
}
A.method.options = {
   some: {
     "default": options
   },
   many: "levels"
}

You've already seen this in action above with tools, but you're highly encouraged to use this pattern with any functions or methods that take an object of options. It's a unified way to override default values, including events.

🚀 Putting It All Together

📦 Main Module

When you write an app or a plugin, you'll probably want to have Javascript file that acts as your main module. In it, you will define the tools, methods, and other things. Here is an example:

/**
 * Streams plugin's front end code
 *
 * @module Streams
 * @class Streams
 */
"use strict";
(function(Q, $) { // $ can be jQuery, $Cash, in any case it's optional

  // defaults are in english, but you can override with Q.Text.get below
  // by convention, modules usually store all user-facing text in Q.text:
  Q.text.Streams = {
    onboarding: {
      prompt: "Fill our your basic information to complete your signup.",
      title: "Basic Information"
    }
  };

  // set default text file for tools and templates
  // with names that start with "Streams/"
  Q.Text.addFor(
    ['Q.Tool.define', 'Q.Template.set'],
    'Streams/', ["Streams/content"]
  );

  Q.Tool.define({
    // specify js and css
    "Streams/chat": {
      js: "{{Streams}}/js/tools/chat.js",
      css: "{{Streams}}/css/tools/chat.css"
    },
    // or define tools as html file (Vue-style)
    "Streams/comments": {
      html: "Streams/html/tools/comments.html"
    }
    // override another module's tool:
    "Users/avatar": "{{Streams}}/js/tools/avatar.js",
  });

  Q.onInit.add(function _Streams_onInit() {
    Q.Text.get('Streams/content').then(text => {
      Q.extend(Q.text.Streams, 10, text);
    });
  });
})(Q, jQuery);

⚖️ Comparison with React, Vue, Angular, Svelte

Feature 🏆Q.js ⚡ (40KB)React 🏗️Vue 🎨Angular 🏛️Svelte 🔥
Bundle Size 📦~40KB gzipped (core + tools + events + routing)42KB + ReactDOM (120KB)~60KB runtime140KB+~50KB compiler/runtime
Build Step 🛠️None (drop-in, works with .html + .js)RequiredRequiredRequiredRequired
Rendering Approach 🎨Direct DOM Updates (No Diffing, No Virtual DOM)Virtual DOM diffingVirtual DOM diffingChange detection via zonesPrecompiled updates (no VDOM, still re-renders)
Performance ⚡Ultra-Fast (Only Updates What’s Needed, No Extra Work)Good, but reconciliation overheadGood, but reconciliation overheadHeavy watchers/zonesFast, but dependencies rerender
Memory Usage 🧠Low (No Virtual DOM, Minimal Garbage Collection)Higher (VDOM objects + GC)Higher (VDOM overhead)Higher (framework runtime)Lower than React, some overhead
State Management 📦Q.Streams + Events (lightweight, no reconciliation)React state / Redux / ContextVuex / PiniaServicesReactive stores
SSR & Hydration 🌍Pre-renders HTML + activates Tools dynamicallyHydrates VDOM (slower)Hydrates VDOMHydrates Angular componentsNeeds hydration after precompile
Component Model 🧩Q.Tools (behaviors on any DOM element)JSX + HooksDirectives + templatesComponents + decoratorsCompiled components
Interactivity & Events 🎭Direct event binding (auto-cleans on removal)Event handlers in JSX (hook dependencies)Event handlers in templatesAngular event bindingsReactive bindings
Batch Updates 🚀Efficient (requestAnimationFrame + .rendering())setState batchingNextTick batchingZone-based batchingDependency-based, no explicit batching
Lazy Loading 💤Built-in (images, tools, components auto-lazyload)Needs 3rd party libsNeeds 3rd party libsBuilt-in, but heavyManual setup
Internationalization 🌐Built-in (Q.Text)3rd party3rd partyi18n module3rd party
Incremental Adoption 🔌Yes (drop-in, enhance existing HTML without rewrite)NoNoNoNo
SEO & Progressive Enhancement 🔍Works with static HTML (enhances dynamically)Needs JS hydrationNeeds JS hydrationNeeds JS hydrationNeeds JS for interactivity
Ecosystem Dependence 🌐All-in-one (routing, templates, events, batching, caching built-in)Needs Router, Redux, i18n, etc.Needs Vuex, Router, i18nHuge framework but still many extra libsNeeds Kit/Sapper + libs
Learning Curve 📚Simple (declarative, minimal magic)Medium-high (hooks, context, JSX)Medium (directives, reactivity caveats)High (decorators, DI, RxJS)Medium
Best For ✅High-performance apps, real-time dashboards, low-latency UI, social platformsFull-scale apps, large component hierarchiesSmall-to-medium apps, good DXEnterprise-scale appsSmall-to-medium apps, hobby projects

📊 Metrics.js — Standalone Telemetry

This repo also includes Metrics.js — a standalone telemetry library that tracks scroll depth, section engagement, and video/audio playback across 9 embed providers. No dependencies, no build step, works on any website.

<script src="https://unpkg.com/@qbix/q/dist/Metrics.js"></script>
<script>
Metrics.init();  // sends to invites.to by default — or pass {endpoint: '/your-own'}
Metrics.ScrollTracker.init({ sections: 'h2[id]' });
Metrics.MediaTracker.init(); // auto-discovers YouTube, Vimeo, SoundCloud, Wistia, JW Player, Dailymotion, Spotify, Twitch, Muse.ai
</script>

Unique features:

  • Watched seconds — tracks unique seconds of video/audio covered, not just total playback. Seeking back and replaying doesn't inflate the number.
  • Section engagement — knows which heading the user is actually reading, not just how far they scrolled.
  • Persistent visitor ID — localStorage with sessionStorage fallback. ITP-safe because it's first-party storage.
  • 9 media providers — auto-detected, APIs loaded on demand, no configuration needed.
  • Zero Q.js dependency — works completely standalone. But if Q.js is loaded, Metrics automatically gains CSRF tokens, Q.Page SPA tracking, Q/tabs and Q/columns auto-hooks, and debounced server-side state updates.

👉 Full Metrics.js documentation →

From Metrics.js to the Qbix Platform

Metrics.js is the lightweight entry point. For the full server-side stack — visit attribution, referral chain walking, multi-level commissions, real-time WebSocket updates, and per-component Merkle-tree cache invalidation — install the Qbix Platform with the Metrics plugin.

The platform includes a high-performance webserver (Cosmopolitan C binary) that maintains a Merkle tree of rendered component hashes. When data changes, only the affected components re-render — not the whole page. Combined with static site generation for CDN caching and Streams/check for per-stream change detection over WebSocket, you get sub-second updates with near-zero server load. No PHP required for serving — the webserver handles everything.

Qbix/Q.js

All-In-One Front-End Web Framework from Qbix, alternative to jQuery, Angular, Vue, Ember etc.

JavaScript

31

141 commits

updated Sep 25, 2026

See the code

README

Q.js

All-In-One Front-End Web Framework from Qbix, alternative to jQuery, Angular, Vue etc.

Size: ~40KB (Minified + GZipped), compare to other frameworks

How to use: copy contents of dist into your project, and then include it like this:

File TypeCode to Use
.html files<script type="module" src="https://unpkg.com/@qbix/q/dist/Q.min.js">
.js or .ts filesimport Q from 'https://unpkg.com/@qbix/q/dist/Q.min.js';
DescriptionFull documentation here: https://qbix.com/platform/guide/javascript

This is part of the much larger full-stack Qbix Platform that contains many pre-built reusable tools, plugins, and requires PHP and Node.js on the back-end. If you want to build an entire full-stack social network like Facebook you're well-advised to go with that. But if you just want to use the lightweight front-end core, with your own back-end and other frameworks, then start with this framework here.

🌟 Advantages of Q.js vs other frameworks

1. No build step
React/Vue/Angular need bundlers, transpilers, tree-shakers, and hydration layers. Q.js works with plain .html and .js — drop it in and it runs.

2. Tiny footprint (~40KB gzipped)
Q.minimal.js is smaller than React (even without ReactDOM), Vue runtime, and far smaller than Angular. Yet it packs components, events, routing, caching, batching, i18n, animations, IndexedDB, service workers, and more — all in core.

3. Direct DOM updates (no Virtual DOM overhead)
No diffing, no reconciler. Q.js uses requestAnimationFrame and .rendering() hooks for high-performance, granular updates.

4. Autoloading methods & tools
Any method, template, or tool can be defined in its own file and is only loaded when first used. No code-splitting configs, no manual lazy-loading hacks — it’s automatic.

5. Tools = components + behaviors
Like React components or Vue directives, but attachable as behaviors to any DOM element. Multiple tools can live on the same element, making composition more flexible than “one component per node.”

6. Built-in batching & caching
APIs like Q.getter() and Q.batcher() deduplicate, throttle, and combine calls automatically. If multiple parts of the UI request the same object, it’s fetched once and everyone gets the result.

7. Templates that Just Work™
Use Handlebars, .html <template> files (Vue-style), or JS-defined templates. Designers can work in HTML, devs can work in JS — both paths integrate seamlessly.

8. First-class events & lifecycle
Events are observable, chainable (a.and(b), a.or(b), a.until(b)), and auto-clean up when a tool or page unloads. No more memory leaks or dangling listeners.

9. Progressive enhancement & SEO-friendly
Pages can render server-side HTML and Q.js simply activates tools as needed. No “blank page until hydration” problem — works with or without JS.

10. Unified, full-stack philosophy
It’s not just a front-end library. Q.js is part of the larger Qbix platform, which powers real social apps (with accounts, feeds, groups, payments, etc.). You can start small with Q.minimal.js, and later plug into the full stack without rewriting.

Q.js is smaller than jQuery, faster than React, easier than Vue, and more complete than Angular.

🔍 Features

Despite its size, Q.js implements many features not found in other front-end frameworks. Here is an overview of the main ones:

Class NameDescription
Q.Toolreusable components, activate with Q.activate(element)
Q.PageHTML pages, for your SPA, routes, browser history
Q.Eventevents and handlers, loaded and unloaded automatically
Q.Templatefor rendering templates, integrates with Handlebars
Q.Textfor loading internationalized translations and text for templates
Q.Methoddefines methods loading JS files asynchronously as needed
Q.Visualmanaging the front end interface, standard hints, as well as Q.Mask
Q.Animationfor animating using native Javascript animation
Q.Audiospeaking, loading and playing audio, etc.
Q.IndexedDBfor easy interaction with the built-in IndexedDB
Q.ServiceWorkerto manage service workers in a standard way
RequestsQ.request(), Q.handle(), Q.loadUrl(), Q.addScript(), Q.addStylesheet()
FlowQ.chain(), Q.getter(), Q.batcher(), Q.promisify(), Q.debounce()
HelpersQ.find(), Q.activate(), Q.cookie(), Q.handle()

Unlike JSX, HTMX or AlpineJS, your HTML markup stays standards-compliant, but now can look like:

<q-each person="@{page.people}" key="person.id">
  <p>${index}: @{person.name}</p>

  <q-if test="@{person.vip}">
    <strong>VIP</strong>
    <q-else-if person-age=">= 18">
      <span>Adult</span>
    </q-else-if>
    <q-else>
      <span>Under 18</span>
    </q-else>
  </q-if>
</q-each>

Overview

Main functionality

// Core
Q
  .init(options)
  .activate(element, options)
  .replace(element, source)
  .extend(target, ...sources)
  .copy(object, fields, levels)
  .exports(fn)
  .require(src, callback)
  .import(src)
  .handle(eventOrURL, context, args)

// Events
Q.Event
  .add(handler, key)
  .set(handler, key)
  .remove(key)
  .removeAllHandlers()
  .factory(name)
  .and(otherEvent)
  .or(otherEvent)
  .until(otherEvent)
  .then()
  .filter(testFn)
  .map(transformFn)
  .debounce(ms)
  .throttle(ms)
  .queue(ms)

// Tools
Q.Tool
  .define(name, constructor, defaults, methods)
  .prepare(elementOrTag, toolName, options)
  .remove(element)
  .clear(container)
  .onActivate(toolName)

// Pages
Q.Page
  .onLoad(urlOrName)
  .onUnload(urlOrName)
  .push(url, options)
  .pop()
  .currentUrl

// Templates
Q.Template
  .set(name, template, options)
  .remove(name)
  .compile(source, type, options)
  .render(name, fields, callback, options)
  .load(name, callback, options)

// Text / i18n
Q.Text
  .setLanguage(lang, locale)
  .set(name, data, options)
  .get(name, callback, options)
  .addFor(events, prefix, names)
  .addedFor(event, name, options)

// Methods
Q.Method
  .define(methods, namespace, closureFn)
  .options

// Visual
Q.Visual
  .intersection(a, b)
  .scrollLeft()
  .scrollTop()
  .windowWidth()
  .windowHeight()
  .boundingRect(element, classFilter, includeHidden)
  .getX(event, index)
  .getY(event, index)
  .shouldOpenInNewWindow(event)

// Animation
Q.Animation
  .play(fn, options)
  .pause()
  .jump(timeOrFrame)
  .rewind()
  .nextFrame()

// Audio
Q.Audio
  .play(src, options)
  .pause(id)
  .stop(id)
  .speak(text, options)

// IndexedDB
Q.IndexedDB
  .open(name, version, upgradeCallback)
  .get(store, key)
  .put(store, value)
  .remove(store, key)
  .clear(store)

// Service Workers
Q.ServiceWorker
  .register(script, options)
  .unregister(scope)
  .message(handler)

// Requests
Q.request(url, slotName, callback, options)
Q.loadUrl(url, options)
Q.addScript(src, callback, options)
Q.addStylesheet(href, options)
Q.removeStylesheet(href)
Q.cookie(name, value, options)

// Flow
Q.chain(tasks, callback)
Q.getter(original, options)
Q.batcher(batchFn, options)
Q.promisify(fn, useThis, callbackIndex)
Q.debounce(fn, ms)

📄 Pages

The concept of Pages is tied to HTTP resources and URLs in browsers.

Q.Page class can be used to manage pages and browser page history, loading and unloading their contents, stylesheets, etc. This happens automatically when you call Q.handle(url) or Q.loadUrl(url). The server-side can send a JSON payload with keys like scripts, stylesheets, etc. and the framework will call Q.addScript() and Q.addStylesheet() to add any new scripts and stylesheets, and Q.removeStylesheet() for any stylesheets that are not in the new page.

Pages can be divided into named slots (e.g. "navigation", "content") so that only parts of a page are requested from the server. The server typically responds with JSON containing slots, which is a map of {slotName: html} pairs, containing new HTML content. Then Q.replace() is used on slots that should be replaced, and finally Q.activate() is called to activate any new tools.

Here is how you add code to run when a page loads and before it unloads:

Q.page('Namespace/action', function () {
  // runs when page is loaded
  return function () {
    // runs before page is unloaded
  };
});

Q.Page.onLoad and Q.Page.onUnload are events that occur when pages are loaded an unloaded.

Q.Page.push(), Q.Page.pop() and Q.Page.currentUrl works with browser history.

More information: https://qbix.com/platform/guide/pages

📝 Templates

You can dynamically create elements in a React-like way, such as this:

Q.element('div', {id: "foo", "class": "bar baz"}, [
   Q.element('img', {src: "foo.png"}),
   Q.element('img', {src: "bar.png"})
]); 

Instead, you can define Handlebars templates like this:

Q.Template.set("Namespace/some/name", `put your template here`);

And then render them later like this:

Q.Template.render("Namespace/some/name").then(html => Q.replace(element, html));

Rendering templates that haven't been set yet causes requests to autoload from inside Q.Template.load.options.dir

🛠️ Tools

Tools represent re-usable components in Q.js -- just like in other front-end libraries, except Tools are only one part of a unified framework!

Here's how to define new types of Tools. Normally you'd define each one in its own file:

File: Namespace/js/tools/cool/name.js:

Q.Tool.define("Namespace/cool/name", constructor, defaultOptions, methods);

File: Namespace/js/tools/another.js:

Q.Tool.define("Namespace/another", function (options) {
  this.refresh(); // call method of tool
}, {
  x: 1,
  y: 2
}, {
  refresh: function () {
    this.state; // copy of options
    this.element; // the element it was activated on
    this.renderTemplate('Namespace/another/view', this.state,
      function (html, elements, tools) {
        // now this.x and this.y point to elements from
        // the template that was rendered, while
        this.element.forEachTool('Namespace/cool/name', function () {
            // run whenever a child tool activates
        });
      }, {
        some: options,
        'Namespace_cool_name_tool': {
            some: childToolOptions
        }
    });
    // this is how we handle in-place updates if x or y changes:
    this.rendering([x, y], (changed, previous, timestamp) => {
      Q.replace(this.elements.x, x); // very quick
      Q.replace(this.elements.y, y); // very quick
      this.element.addClass('updated_flash'); // some CSS effect
    });
    // to trigger these, anyone can simply call tool.stateChanged('x')
  },
  Q: {
    onInit: function () {
      // all child tools have been initialized
    }
    beforeRemove: function () {
      // cleanup, but see Events section!
    }
  }
});

// define a template with a child tool
Q.Template.set("Namespace/another/view",
   `<span class="Namespace_another_x">{{x}}</span>
    <span class="Namespace_another_y">{{y}}</span>
    {{{tool "Namespace/cool/name" "some-child-id" x=x y=3 z="foo"}}}`,
   {
      "elements": {
         "x": ".Namespace_another_x",
         "y": ".Namespace_another_y"
      }
   }
);

Optionally, you can also define tools in HTML files, Vue-style, which may result in nicer syntax highlighting for both the HTML templates and the Javascript:

<template id="Namespace/another/view">
   <span class="Namespace_another_x">{{x}}</span>
</template>
<script>
Q.Tool.define("Namespace/another/x", constructor, defaultOptions, methods);
// you can Q.Template.render("Namespace/another/view") as above
</script>

How to include tools in HTML:

<div class="Q_tool Namespace_tool_name Namespace-another-tool"
  data-namespace-cool-name='{
    "some": { "options": "go"}, "here": 2
  }'
  data-namespace-another='{
    "some": [ "other", options", here" ]
  }'
>
   <p>optionally might give the tool initial content</p>
</div>

How to prepare tools in JS:

// new component:
Q.Tool.prepare('div', toolName, options); // new div
// or add behaviors to existing element:
Q.Tool.prepare(element, toolName, options);
Q.Tool.prepare(element, anotherTool, otherOptions);

Multiple Tools can be defined on the same element. The Q.activate() function uses Q.find() to recursively find all elements with Q_tool CSS class, and then activates the tools in the order they've been defined. Tools are activated parent->child->grandchild, and then initialized grandchild->child->parent. You can pass options at activation-time, too, targeting tools by .classname or #id:

Q.activate(element, {
  '#specific_id': {
    "override": { "some": "options" },
  },
  '.Namespace_another_tool': {
     "some": ["more", "options" ]
  }
}); // finds and activates tools, then initializes them

Whenever you call Q.activate(document.body) it will traverse the whole document body, so it's slightly faster to call Q.activate() on containers where you've recently replaced HTML, rather than the whole document body.

You can remove tools manually before removing elements:

Q.Tool.clear(container); // child elements only
Q.Tool.remove(element); // also on element itself

but you usually don't need to, because Q.replace(element, source) does this for you automatically. The source can be an element, document fragment, or some HTML string.

Some tools might have data-q-retain attributes, causing them to be retained, and not removed and re-activated, if the incoming HTML has the same tool IDs. When a tool is retained, its tool.Q.onRetain event is triggered. However, incoming tools with a data-q-replace attributes replace even tools that had data-q-retain set.

Q.js even overrides $.fn.html() and $.fn.activate() in libraries like jQuery and $cash so that you can call the following to automatically cause old tools to be removed and new tools to be activated inside the element:

$(element).html(html).activate();

When tools are removed, all associated event handlers are removede automaitically! (See Events section below).

More information: https://qbix.com/platform/guide/tools

🧩 Components in HTML

Set Q.Tool.define.components = true before registering tools to give them custom element tags. A tool named Streams/chat becomes <streams-chat>. Attributes become tool options: publisher-id becomes publisherId, and on-custom-event can attach a handler when onCustomEvent is a Q.Event in the tool’s defaults.

The Q/if and Q/each tools let you write conditions, loops, and bindings directly in HTML:

<template id="People/vip">
  <strong>{{person.name}}</strong> has @{person.score} points.
</template>

<q-if test="true">
  <h2>@{page.title}</h2>

  <q-each person="@{page.people}" key="person.id">
    <article title="@{person.name}">
      <h3>${index}: @{person.name}</h3>

      <q-if person-age=">= 18" person-status="'active'">
        <p>Active adult</p>
        <q-else-if test="@{person.vip || person.score >= page.minimum}">
          <p>Special access</p>
        </q-else-if>
        <q-else><p>Waiting for access</p></q-else>
      </q-if>

      <q-if test="@{person.vip}" template="People/vip">
        <q-else><p>Standard member</p></q-else>
      </q-if>
    </article>
  </q-each>
</q-if>

⏰ Events

Add onFoo: new Q.Event() as properties on any object. By convention, the properties are called onFoo, and optionally beforeFoo (before the event occurs).

To trigger an event, call Q.handle(event, context, arguments). This will call all the handlers set on the event. If any of the handlers returns false then all subsequent handlers are skipped and Q.handle returns false. While being handled, event.occurring = true, and afterwards, event.occured = true.

Use onFoo.set(handler, key) to set a handler on an event, or onFoo.add(handler, key) to set it but also call it, if the event already .occurred === true.

You can call onFoo.remove(key) to remove handlers previously set for that key. Or call onFoo.removeAllHandlers() to remove all handlers for that event.

Authors of new Tools and Pages use Q.Tool.define(name, constructor) and Q.page(name, constructor). During the lifetime of a tool or page, outside code may add handlers to some events associated with those tools or pages. Normally, they'd need to follow it up with code to manually remove those events when the tool is removed or page is unloaded, i.e. during tool.Q.beforeRemove and Q.Page.beforeUnload events, respectively.

However, Q.js has a great way to automate the removal of events. Simply pass the tool or true instead of the key, as follows:

Call TypeDescription
event.set(handler, string)need to manually call event.remove(key)
event.set(handler, tool)automatically removed when tool is removed
event.set(handler, true)automatically removed when current page is unloaded
$cash.on(event, tool, handler)automatically calls .off() when tool is removed
$cash.on(event, true, handler)automatically removed when current page is unloaded

Calling set() or add() again with the same String key replaces previous handlers set with that same key. But if the key is not a String (i.e. it's a Q.Tool or true) then the handlers are added to the existing ones, while event.remove(tool) event.remove(true) removes everything added for that tool or current page.

You can define your own event factory very easily using the Q.Event.factory() function. The Event Factory pattern is used in order to create events on demand. For example, here we call an event factory to produce an event and add a handler to be run whenever a tool is activated

Q.Tool.onActivate(toolType) // retrieves or creates new Q.Event
   .add(handler, "MyModule"); // adds the handler with a key

Q.Event also supports Observable Streams / Reactive Events, through methods like

methodthe new event
a.and(b)occurs when both occurred`
a.or(b)occurs when either occurred
a.stop()indicates event won't be occurring anymore
a.until(b)occurs when a occurs, until b starts occurring
a.then()occurs only after a stops occurring
a.filter(test)occurrs only if test returns true
a.map(transform)occurrs after transforming parameters
a.debounce(ms)occurs only during a pause in a being fired
a.throttle(ms)occurs when a occurs, but at most once every ms
a.queue(ms)queues up occurrences to happen every ms

More information: https://qbix.com/platform/guide/eventsClient

⚙️ Methods

There are multiple ways to autoload external tools on demand.

// Analogues of node.js modules
Q.exports(function () { ... }); // in a file
Q.require(src, callback); // loading the file
Q.import(src).then(...); // wrapper around native import

But also, any asynchronous methods that utilize a callback or promise can be autoloaded:

Q.Data = Q.Method.define({
  all: function (a, b) {
    // regular method
  },
  digest: new Q.Method(),
  compress: new Q.Method(),
  decompress: new Q.Method(),
  sign: new Q.Method(),
  verify: new Q.Method(),
}, "Q/Data", function() {
  // pass variables in a closure
  return [Q, something];
}));

and then you can define the methods in files like Q/Data/digest.js:

Q.exports(function (Q, something) { // receive closures from main file
    return function Q_Data_digest(algorithm, payload, callback) {
        // here you have access to both the parameters and the closures!!
        return doStuff.then(function (result) {
            callback && callback(null, result); // callback interface
            return result; // promise interface
        });
    };
});

You should consider using this extensively, to organize your front-end code and load only as needed:

Q.Data.sign(algorithm, payload) // autoload method's code on demand
.then(...) // continue after promise resolves

📂 Objects

Use Q.copy(object, fields, levels) to copy an object. Sub-objects may expose custom .copy() methods to be used.

Do Q.extend(target, levels, obj1, obj2) method is used to modify target, adding from extension.

Do result = Q.extend({}, defaults, levels, obj1, obj2) to overrides some defaults with specific values.

You can extend Q.Event objects, Q.extend(target.onFoo, {"bar": handler)) will call target.onFoo.set(handler, bar). This allows you to pass custom event handlers that will be added to a copy of an event:

Q.Tool.define("Foo/bar", function (options) {
   this.state // this is Q.extend({}, defaults, options);
}, {
  foo: ['a', 'b'],
  bar: ['a', 'b'],
  onFoo: new Event(function () {
     // default handler here
  }, "Foo/bar"), // with sensible key name
  childToolOptions: {
    onBaz: new Event() // empty event
  }
});

Every instance of the Foo/bar tool actually makes a copy of the default options, and then extends them, saving the result in tool.state, ready to use. So suppose you instantiated the tool as follows:

const element = Q.Tool.prepare('div', 'Foo/bar', {
   // override some options when making element
   foo: ['c', 'd'],
   bar: {replace: ['c', 'd']},
   onFoo: {
     "Foo/bar": function () {
       // override default handler, but only for this instance
     }
   }
});
document.body.appendChild(element);
Q.activate(element, {
  // override more options at activation time
  childToolOptions: {
    onBaz: {
      "myCoolPage": function () {
         // add this handler, but only for this instance
      }
    }
  }
});

Other tool instances won't have these handlers added. They'll have a copy of the default options, including the event objects, that wasn't extended with these.

Also notice what Q.extend() does with arrays. The foo above would become ['a', 'b', 'c', 'd'] while the bar would become ['c', 'd'], because the array was being replaced by an object with a key "replace", so it replaces the array with the given value (another array).

➕ Functions

By convention, methods that take options have default options defined in the options property on the method itself. When called, a copy of the default options extended with any options passed to the method. Example:

A.method = function (a, b, options) {
   const o = Q.extend({}, A.method.options, 10, options);
}
A.method.options = {
   some: {
     "default": options
   },
   many: "levels"
}

You've already seen this in action above with tools, but you're highly encouraged to use this pattern with any functions or methods that take an object of options. It's a unified way to override default values, including events.

🚀 Putting It All Together

📦 Main Module

When you write an app or a plugin, you'll probably want to have Javascript file that acts as your main module. In it, you will define the tools, methods, and other things. Here is an example:

/**
 * Streams plugin's front end code
 *
 * @module Streams
 * @class Streams
 */
"use strict";
(function(Q, $) { // $ can be jQuery, $Cash, in any case it's optional

  // defaults are in english, but you can override with Q.Text.get below
  // by convention, modules usually store all user-facing text in Q.text:
  Q.text.Streams = {
    onboarding: {
      prompt: "Fill our your basic information to complete your signup.",
      title: "Basic Information"
    }
  };

  // set default text file for tools and templates
  // with names that start with "Streams/"
  Q.Text.addFor(
    ['Q.Tool.define', 'Q.Template.set'],
    'Streams/', ["Streams/content"]
  );

  Q.Tool.define({
    // specify js and css
    "Streams/chat": {
      js: "{{Streams}}/js/tools/chat.js",
      css: "{{Streams}}/css/tools/chat.css"
    },
    // or define tools as html file (Vue-style)
    "Streams/comments": {
      html: "Streams/html/tools/comments.html"
    }
    // override another module's tool:
    "Users/avatar": "{{Streams}}/js/tools/avatar.js",
  });

  Q.onInit.add(function _Streams_onInit() {
    Q.Text.get('Streams/content').then(text => {
      Q.extend(Q.text.Streams, 10, text);
    });
  });
})(Q, jQuery);

⚖️ Comparison with React, Vue, Angular, Svelte

Feature 🏆Q.js ⚡ (40KB)React 🏗️Vue 🎨Angular 🏛️Svelte 🔥
Bundle Size 📦~40KB gzipped (core + tools + events + routing)42KB + ReactDOM (120KB)~60KB runtime140KB+~50KB compiler/runtime
Build Step 🛠️None (drop-in, works with .html + .js)RequiredRequiredRequiredRequired
Rendering Approach 🎨Direct DOM Updates (No Diffing, No Virtual DOM)Virtual DOM diffingVirtual DOM diffingChange detection via zonesPrecompiled updates (no VDOM, still re-renders)
Performance ⚡Ultra-Fast (Only Updates What’s Needed, No Extra Work)Good, but reconciliation overheadGood, but reconciliation overheadHeavy watchers/zonesFast, but dependencies rerender
Memory Usage 🧠Low (No Virtual DOM, Minimal Garbage Collection)Higher (VDOM objects + GC)Higher (VDOM overhead)Higher (framework runtime)Lower than React, some overhead
State Management 📦Q.Streams + Events (lightweight, no reconciliation)React state / Redux / ContextVuex / PiniaServicesReactive stores
SSR & Hydration 🌍Pre-renders HTML + activates Tools dynamicallyHydrates VDOM (slower)Hydrates VDOMHydrates Angular componentsNeeds hydration after precompile
Component Model 🧩Q.Tools (behaviors on any DOM element)JSX + HooksDirectives + templatesComponents + decoratorsCompiled components
Interactivity & Events 🎭Direct event binding (auto-cleans on removal)Event handlers in JSX (hook dependencies)Event handlers in templatesAngular event bindingsReactive bindings
Batch Updates 🚀Efficient (requestAnimationFrame + .rendering())setState batchingNextTick batchingZone-based batchingDependency-based, no explicit batching
Lazy Loading 💤Built-in (images, tools, components auto-lazyload)Needs 3rd party libsNeeds 3rd party libsBuilt-in, but heavyManual setup
Internationalization 🌐Built-in (Q.Text)3rd party3rd partyi18n module3rd party
Incremental Adoption 🔌Yes (drop-in, enhance existing HTML without rewrite)NoNoNoNo
SEO & Progressive Enhancement 🔍Works with static HTML (enhances dynamically)Needs JS hydrationNeeds JS hydrationNeeds JS hydrationNeeds JS for interactivity
Ecosystem Dependence 🌐All-in-one (routing, templates, events, batching, caching built-in)Needs Router, Redux, i18n, etc.Needs Vuex, Router, i18nHuge framework but still many extra libsNeeds Kit/Sapper + libs
Learning Curve 📚Simple (declarative, minimal magic)Medium-high (hooks, context, JSX)Medium (directives, reactivity caveats)High (decorators, DI, RxJS)Medium
Best For ✅High-performance apps, real-time dashboards, low-latency UI, social platformsFull-scale apps, large component hierarchiesSmall-to-medium apps, good DXEnterprise-scale appsSmall-to-medium apps, hobby projects

📊 Metrics.js — Standalone Telemetry

This repo also includes Metrics.js — a standalone telemetry library that tracks scroll depth, section engagement, and video/audio playback across 9 embed providers. No dependencies, no build step, works on any website.

<script src="https://unpkg.com/@qbix/q/dist/Metrics.js"></script>
<script>
Metrics.init();  // sends to invites.to by default — or pass {endpoint: '/your-own'}
Metrics.ScrollTracker.init({ sections: 'h2[id]' });
Metrics.MediaTracker.init(); // auto-discovers YouTube, Vimeo, SoundCloud, Wistia, JW Player, Dailymotion, Spotify, Twitch, Muse.ai
</script>

Unique features:

  • Watched seconds — tracks unique seconds of video/audio covered, not just total playback. Seeking back and replaying doesn't inflate the number.
  • Section engagement — knows which heading the user is actually reading, not just how far they scrolled.
  • Persistent visitor ID — localStorage with sessionStorage fallback. ITP-safe because it's first-party storage.
  • 9 media providers — auto-detected, APIs loaded on demand, no configuration needed.
  • Zero Q.js dependency — works completely standalone. But if Q.js is loaded, Metrics automatically gains CSRF tokens, Q.Page SPA tracking, Q/tabs and Q/columns auto-hooks, and debounced server-side state updates.

👉 Full Metrics.js documentation →

From Metrics.js to the Qbix Platform

Metrics.js is the lightweight entry point. For the full server-side stack — visit attribution, referral chain walking, multi-level commissions, real-time WebSocket updates, and per-component Merkle-tree cache invalidation — install the Qbix Platform with the Metrics plugin.

The platform includes a high-performance webserver (Cosmopolitan C binary) that maintains a Merkle tree of rendered component hashes. When data changes, only the affected components re-render — not the whole page. Combined with static site generation for CDN caching and Streams/check for per-stream change detection over WebSocket, you get sub-second updates with near-zero server load. No PHP required for serving — the webserver handles everything.

Languages

JavaScript

99.9%