vanjs-org / van

🍦 VanJS: World's smallest reactive UI framework. Incredibly Powerful, Insanely Small - Everyone can build a useful UI app in an hour.
https://vanjs.org
MIT License
3.77k stars 87 forks source link

A way to make VanJS even smaller #306

Closed ethanlal04 closed 4 months ago

ethanlal04 commented 4 months ago

Hey, Im surprised no one did this but here's the idea: Instead of using descriptive variable names in the implementation code, we can use single-letter characters. This should dramatically reduce the file size. Ive even tried it below:

let a = Object.getPrototypeOf;
let b,
  c,
  d,
  e,
  f = { isConnected: 1 };
let g = 1000,
  h,
  i = {};
let j = a(f),
  k = a(a),
  _;

let l = (set, s, f, waitMs) =>
  (set ?? (setTimeout(f, waitMs), new Set())).add(s);

let m = (f, deps, arg) => {
  let prevDeps = d;
  d = deps;
  try {
    return f(arg);
  } catch (e) {
    console.error(e);
    return arg;
  } finally {
    d = prevDeps;
  }
};

let n = (l) => l.filter((b) => b._dom?.isConnected);

let o = (d) =>
  (h = l(
    h,
    d,
    () => {
      for (let s of h)
        (s._bindings = n(s._bindings)), (s._listeners = n(s._listeners));
      h = _;
    },
    g
  ));

let p = {
  get val() {
    d?._getters?.add(this);
    return this.rawVal;
  },

  get oldVal() {
    d?._getters?.add(this);
    return this._oldVal;
  },

  set val(v) {
    d?._setters?.add(this);
    if (v !== this.rawVal) {
      this.rawVal = v;
      this._bindings.length + this._listeners.length
        ? (c?.add(this), (b = l(b, this, y)))
        : (this._oldVal = v);
    }
  },
};

let q = (initVal) => ({
  __proto__: p,
  rawVal: initVal,
  _oldVal: initVal,
  _bindings: [],
  _listeners: [],
});

let r = (f, dom) => {
  let deps = { _getters: new Set(), _setters: new Set() },
    binding = { f },
    prevNewDerives = e;
  e = [];
  let newDom = m(f, deps, dom);
  newDom = (newDom ?? document).nodeType ? newDom : new Text(newDom);
  for (let d of deps._getters)
    deps._setters.has(d) || (o(d), d._bindings.push(binding));
  for (let l of e) l._dom = newDom;
  e = prevNewDerives;
  return (binding._dom = newDom);
};

let s = (f, s = q(), dom) => {
  let deps = { _getters: new Set(), _setters: new Set() },
    listener = { f, s };
  listener._dom = dom ?? e?.push(listener) ?? f;
  s.val = m(f, deps, s.rawVal);
  for (let d of deps._getters)
    deps._setters.has(d) || (o(d), d._listeners.push(listener));
  return s;
};

let t = (dom, ...children) => {
  for (let c of children.flat(Infinity)) {
    let protoOfC = a(c ?? 0);
    let child = protoOfC === p ? r(() => c.val) : protoOfC === k ? r(c) : c;
    child != _ && dom.append(child);
  }
  return dom;
};

let u = (ns, name, ...args) => {
  let [props, ...children] = a(args[0] ?? 0) === j ? args : [{}, ...args];
  let dom = ns
    ? document.createElementNS(ns, name)
    : document.createElement(name);
  for (let [k, v] of Object.entries(props)) {
    let getPropDescriptor = (proto) =>
      proto
        ? Object.getOwnPropertyDescriptor(proto, k) ??
          getPropDescriptor(a(proto))
        : _;
    let cacheKey = name + "," + k;
    let propSetter =
      i[cacheKey] ?? (i[cacheKey] = getPropDescriptor(a(dom))?.set ?? 0);
    let setter = k.startsWith("on")
      ? (v, oldV) => {
          let event = k.slice(2);
          dom.removeEventListener(event, oldV);
          dom.addEventListener(event, v);
        }
      : propSetter
      ? propSetter.bind(dom)
      : dom.setAttribute.bind(dom, k);
    let protoOfV = a(v ?? 0);
    k.startsWith("on") || (protoOfV === k && ((v = s(v)), (protoOfV = p)));
    protoOfV === p ? r(() => (setter(v.val, v._oldVal), dom)) : setter(v);
  }
  return t(dom, ...children);
};

let v = (ns) => ({ get: (_, name) => u.bind(_, ns, name) });
let w = new Proxy((ns) => new Proxy(u, v(ns)), v());

let x = (dom, newDom) =>
  newDom ? newDom !== dom && dom.replaceWith(newDom) : dom.remove();

let y = () => {
  let iter = 0,
    derivedStatesArray = [...b].filter((s) => s.rawVal !== s._oldVal);
  do {
    c = new Set();
    for (let l of new Set(
      derivedStatesArray.flatMap((s) => (s._listeners = n(s._listeners)))
    ))
      s(l.f, l.s, l._dom), (l._dom = _);
  } while (++iter < 100 && (derivedStatesArray = [...c]).length);
  let changedStatesArray = [...b].filter((s) => s.rawVal !== s._oldVal);
  b = _;
  for (let b of new Set(
    changedStatesArray.flatMap((s) => (s._bindings = n(s._bindings)))
  ))
    x(b._dom, r(b.f, b._dom)), (b._dom = _);
  for (let s of changedStatesArray) s._oldVal = s.rawVal;
};

let z = (dom, f) => x(dom, r(f, dom));

export default { add: t, tags: w, state: q, derive: s, hydrate: z };

Here I renamed only variables in the global scope, but this can also be applied to local variables. The number of characters (ignoring linebreaks and whitespace) dropped from 3817 to 3044. That's a lot.

Funnily there were exactly 26 variables in the global scope which could be mapped to the 26 letters of the alphabet.

I know this will make maintenance impossible, but we can put a table at the start of the file that maps the alphabet to its descriptive name (or even a short sentence).

Like this:

/*
Name  Description  More info
a     protoOf      Object.getPrototypeOf shorthand
b     changedStates  .
c     derivedStates  .
.    .
.    .
.    .
*/

Also multiple local comment tables for variables of local scopes

All this can be removed before building for production.

U guys already use let instead of const for naming variables, I don't see why this cant be done. Thoughts?

Tao-VanJS commented 4 months ago

Thanks @ethanlal04 for the suggestion!

We use Terser to minify the script, which does variable renaming, among other size reduction techniques. You can see the minified script of the recent version as an example. In addition, the size that people care about the most, is the gzipped minified bundle (VanJS's gzipped minified bundle is 1.0kB), as gzip compression is usually performed on the server before the HTTP transmission.

ethanlal04 commented 4 months ago

Thanks a lot, I didnt know that. I'll close this then.