DEV Community

Cover image for 1KB Frontend Library
Fedor
Fedor

Posted on • Edited on

67 5 6 6 11

1KB Frontend Library

Today’s Challenge: Build a 1KB Frontend Library

Let’s tackle an exciting challenge today: creating a frontend library that’s just 1 kilobyte in size. I’m talking about a "disappearing framework" — not like Svelte, which only "disappears" after compilation. No build tools, no bloated node_modules folder hogging your SSD. Just a few lightweight JavaScript functions you can copy, paste, and use right away.

Buckle up!


Reactivity with Signals

By 2025, the frontend world has largely agreed on one thing: signals for reactivity. Almost every major framework has its own version of signals — like Vue’s ref() or Svelte’s $state rune.

If you’re new to signals, don’t worry. Just remember two key concepts:

  1. Signals: Reactive values that can be read and updated.
  2. Effects: Functions that depend on signals. When a signal changes, its dependent effects automatically re-run.

A Tiny Signals Implementation

Our compact signals implementation is inspired by Andrea Giammarchi’s excellent article on signals. If you’re curious about the nitty-gritty, I highly recommend giving it a read.

{

const effects = [Function.prototype];
const disposed = new WeakSet();

function signal(value) {
  const subs = new Set();
  return (newVal) => {
    if (newVal === undefined) {
      subs.add(effects.at(-1));
      return value;
    }
    if (newVal !== value) {
      value = newVal?.call ? newVal(value) : newVal;
      for (let eff of subs) disposed.has(eff) ? subs.delete(eff) : eff();
    }
  };
}

function effect(fn) {
  effects.push(fn);
  try {
    fn();
    return () => disposed.add(fn);
  } finally {
    effects.pop();
  }
}

}

function computed(fn) {
  const s = signal();
  s.dispose = effect(() => s(fn()));
  return s;
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • We use a block scope ({}) to keep our variables out of the global namespace. This is handy when modules aren’t an option.
  • The signal function creates a reactive value. It returns a function that acts as both a getter and setter:
    • If called without arguments, it returns the current value and subscribes the active effect to the signal.
    • If called with a new value, it updates the signal and triggers all subscribed effects (unless they’re disposed).
  • The effect function registers a callback that runs immediately and re-runs whenever any of its dependent signals change.
  • The computed function creates a derived signal — a reactive value that is recalculated every time it's dependencies change.

Example Usage:

const count = signal(0); // Create a signal with initial value 0

effect(() => {
  console.log(`Count is: ${count()}`); // Log the current value of the signal
});

count(1); // Update the signal, which triggers the effect and logs "Count is: 1"
count(2); // Update again, logs "Count is: 2"
Enter fullscreen mode Exit fullscreen mode

Reactive HTML Templates

Now, let’s add some templating and rendering magic. We’ll create a tagged template function, html, that parses HTML strings and dynamically binds reactive values to the DOM.

{

function html(tpl, ...data) {
  const marker = "\ufeff";
  const t = document.createElement("template");
  t.innerHTML = tpl.join(marker);
  if (tpl.length > 1) {
    const iter = document.createNodeIterator(t.content, 1 | 4);
    let n,
      idx = 0;
    while ((n = iter.nextNode())) {
      if (n.attributes) {
        if (n.attributes.length)
          for (let attr of [...n.attributes])
            if (attr.value == marker) render(n, attr.name, data[idx++]);
      } else {
        if (n.nodeValue.includes(marker)) {
          let tmp = document.createElement("template");
          tmp.innerHTML = n.nodeValue.replaceAll(marker, "<!>");
          for (let child of tmp.content.childNodes)
            if (child.nodeType == 8) render(child, null, data[idx++]);
          n.replaceWith(tmp.content);
        }
      }
    }
  }
  return [...t.content.childNodes];
}

const render = (node, attr, value) => {
  const run = value?.call
    ? (fn) => {
        let dispose;
        dispose = effect(() =>
          dispose && !node.isConnected ? dispose() : fn(value())
        );
      }
    : (fn) => fn(value);
  if (attr) {
    node.removeAttribute(attr);
    if (attr.startsWith("on")) node[attr] = value;
    else
      run((val) => {
        if (attr == "value" || attr == "checked") node[attr] = val;
        else
          val === false
            ? node.removeAttribute(attr)
            : node.setAttribute(attr, val);
      });
  } else {
    const key = Symbol();
    run((val) => {
      const upd = Array.isArray(val)
        ? val.flat()
        : val !== undefined
        ? [document.createTextNode(val)]
        : [];
      for (let n of upd) n[key] = true;
      let a = node,
        b;
      while ((a = a.nextSibling) && a[key]) {
        b = upd.shift();
        if (a !== b) {
          if (b) a.replaceWith(b);
          else {
            b = a.previousSibling;
            a.remove();
          }
          a = b;
        }
      }
      if (upd.length) (b || node).after(...upd);
    });
  }
}

}
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • The html function returns an array of DOM nodes.
  • It supports dynamic attributes, text content, child nodes, and event listeners using the on* syntax.
  • If the provided value is a function (or a signal itself) it sets up an effect which is re-run to update the DOM.

Example Usage:


  // Reactive state
  const count = signal(0);

  // Render the app
  const app = html`<div>
      <h1>Counter: ${count}</h1>
      <button onclick=${() => count((val) => val + 1)}>Increment</button>
      <button onclick=${() => count((val) => val - 1)}>Decrement</button>
    </div>`;

  // Mount the app to the DOM
  document.body.append(...app);

Enter fullscreen mode Exit fullscreen mode

A More Complex Example: A Todo App

Check out this interactive Todo app built with our tiny library. It’s a great example of what you can achieve with just a few lines of code.


What’s Next?

In the next installment, we’ll add efficient list re-rendering with just one function. Stay tuned! 🚀

Top comments (16)

Collapse
 
benny00100 profile image
Benny Schuetz • Edited

Awesome project, especially because I think todays frameworks are way too bloated.

Btw. I also created a small anti-framework called InfrontJS.

I am really interested what you are coming up with. Maybe I can pick up some ideas for my framework if you dont mind ;-)

Collapse
 
fedia profile image
Fedor

Oh, so these things are called anti-frameworks - good to know! Awesome site, and it's great to see your framework evolving! I still can’t manage to work on pet-projects with that level of discipline.

Collapse
 
dariomannu profile image
Dario Mannu

haha, I started the same way, 100LoC, template literals+observables (👎👎👎 signals lol), used to call it the "No Framework"... then it evolved into a UI library, slowly growing into a full, modular framework (going to happen to all of you, guys, it's a law of nature)...

Nice work, guys, both!

Thread Thread
 
benny00100 profile image
Benny Schuetz

Good to meet some "same-minded" people here 👍

Collapse
 
benny00100 profile image
Benny Schuetz

Thanks for your head-ups. Yeah, I came across the term "anti-framework" on a blogpost somewhere and somehow liked it ;-)

Collapse
 
marceloxp profile image
Marcelo de Souza Lima

Create a comparative table with react-based "black spells". Your framework can win by simplicity. 😁

Collapse
 
daniel15 profile image
Daniel Lo Nigro

Very nice post! Thanks for writing it.

I used to do something like this, but I've moved back towards server-side rendering and inserting chunks of HTML on to the page, similar to how htmx does it. Works well in a surprisingly large number of cases, especially in cases where you'd have to persist the data server-side anyways. One of the things I miss with an approach like that is optimistic updates on the client side, which your library would handle well (render with the new data, then on error revert the data back to the old data).

Collapse
 
thescottyjam profile image
theScottyJam

I love the idea of copy-paste tools, for when you're working on a smaller project and don't really need all the weight of a full-fledged framework. I also love how small you were able to get this.

I had attempted to make a copy-paste framework once upon a time. It ended up being 1.5 kb gzipped. You got me beat :). But it looks like fundamentally, we took a similar approach, namely building everything on top of signals and an html template tag.

I hope one day in the future these sorts of things can just be in native JavaScript.

Collapse
 
fedia profile image
Fedor

Thanks! Really cool to see similar projects from like-minded devs! snap.js has way more features - no wonder the size is a bit bigger.

Collapse
 
intermundos profile image
intermundos

Great attempt.
Wondering if that would work in complex applications…?

Collapse
 
fedia profile image
Fedor

This implementation of signals definitely has its limitations. They're described in the article I linked to. Other than that, it's pretty standard and basic DOM manipulation. I'll write a separate article about optimizing list rendering.

Collapse
 
artydev profile image
artydev

Great. I am militant of minimalisme🙂

Collapse
 
dariomannu profile image
Dario Mannu

now I wonder why so many people (incl. myself) keep re-inventing the same no-framework/anti-framework (ok, that's easy, that's the React/Angular Fatigue, right?), instead of teaming up and building something big together (ego, perhaps? Everybody wanting to become "the man" who changed the world and get 100% of the credit? Other realistic explanations? I did it because I needed a framework and was in total disagreement with the approach taken by most existing ones. How about you guys?)...
So in the end there are dozens of talented creators around who built 100% of something nobody uses and knows about, even if it's a brilliant product that would deserve taking over the market...

Collapse
 
fedia profile image
Fedor

To dominate the market, you need marketing. So, in my opinion, it’s not about ego or chasing GitHub stars. It’s more about sharing what you’ve built because you genuinely enjoy it and find it interesting.

Collapse
 
joo_pedroviana_14549d71 profile image
João Pedro Viana

useless

Collapse
 
kien5436 profile image
Phạm Trung Kiên

after every framework, I see react still right from the beginning

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post