(() => {
// Aliases to aid in minification:
var doc = document;
var querySelectorAll = (e, s) => e.querySelectorAll(s);
// Plugins, list of functions that take the css and return a new css
var pluginsPre = [];
var pluginsPost = [];
/**
* x-style
* @param {string} attr - HTML attribute that contains the css, usually "css"
* @param {boolean} [noMutate] - Don't mutate the DOM by adding attributes
* @param {boolean} [discreteStyles] - Saves css in a discrete style element per DOM element
*/
var xstyle = (attr, noMutate, discreteStyles) => {
var styleEl;
var style = [];
var selectorCount = 0;
var attributeForSelector = `${attr}-match`;
var elementIdAttribute = `${attr}-id`;
var elementIdSeed = 0;
var processedCss = new Map(); // Map<cssString, renderSeed>
var allProcessedCss = new Map(); // Map<cssString, renderSeed>
var elementProcessedCss = new Map(); // Map<elementId, cssString>
var elementIdNodes = new Map(); // Map<elementId, node>
var elementIdStyleNode = new Map(); // Map<elementId, styleNode>
var idsToUpdate = new Set(); // DOM elements that have new CSS to apply
var styleElementsToDelete = new Set(); // We store these in a separate buffer to minimize DOM operations
var removingDomElements = false;
if (discreteStyles) {
// Every 10 seconds, clean up our processed CSS map to free up memory
setInterval(() => processedCss.clear(), 10000);
}
var removeStaleStyleElements = () => {
if (removingDomElements) return;
removingDomElements = true;
requestAnimationFrame(() => {
const elements = Array.from(styleElementsToDelete.values());
styleElementsToDelete.clear();
removingDomElements = false;
for (const el of elements) {
el.remove();
}
});
};
var observer = new MutationObserver((mutations) => {
for (var mutation of mutations) {
if (mutation.type === "attributes") {
processEl(mutation.target);
} else if (mutation.type === "childList") {
for (var el of mutation.addedNodes) {
if (!(el instanceof HTMLElement)) continue;
if (el.hasAttribute(attr)) {
processEl(el);
}
[...querySelectorAll(el, `[${attr}]`)].forEach(processEl);
}
if (discreteStyles) {
for (var el of mutation.removedNodes) {
if (!(el instanceof HTMLElement)) continue;
var id = el[elementIdAttribute];
if (!id) continue;
elementProcessedCss.delete(id);
elementIdNodes.delete(id);
idsToUpdate.delete(id);
const styleNode = elementIdStyleNode.get(id);
if (styleNode) {
elementIdStyleNode.delete(id);
styleElementsToDelete.add(styleNode);
}
}
}
}
}
emitStyle();
});
var emitStyle = () => {
if (discreteStyles) {
const values = Array.from(idsToUpdate.values());
idsToUpdate.clear();
for (const id of values) {
var css = elementProcessedCss.get(id);
if (!css) {
continue;
}
var styleNode = elementIdStyleNode.get(id);
if (!styleNode) {
var styleEl = doc.createElement("style");
styleEl.innerHTML = elementProcessedCss.get(id);
doc.head.appendChild(styleEl);
styleNode = elementIdStyleNode.set(id, styleEl);
continue;
}
styleNode.innerHTML = css;
removeStaleStyleElements();
}
} else if (style.length) {
styleEl = doc.createElement("style");
styleEl.innerHTML = style;
doc.head.appendChild(styleEl);
styleEl = null;
style = "";
}
};
var setAttribute = (el, rawCss) => {
var selectorAttr = `${attributeForSelector}-${processedCss.get(rawCss)}`;
var prop = "__" + attributeForSelector;
if (el[prop]) {
el.removeAttribute(el[prop]);
}
el.setAttribute(selectorAttr, "");
el[prop] = selectorAttr;
return selectorAttr;
};
/**
* Process an element.
* Extract the css from the attribute and add it to the style element.
* If the css has already been processed, either add the attributeForSelector
* or do nothing.
* The style element is added to the head on the next microtask.
* @param {HTMLElement} el
*/
var processEl = (el) => {
var rawCss = el.getAttribute(attr);
el[elementIdAttribute] = el[elementIdAttribute] || ++elementIdSeed;
if (discreteStyles) {
el.setAttribute(elementIdAttribute, el[elementIdAttribute]);
}
var css;
if (!rawCss || processedCss.has(rawCss)) {
if (!noMutate) {
setAttribute(el, rawCss);
}
return;
}
processedCss.set(rawCss, ++selectorCount);
allProcessedCss.set(rawCss, selectorCount);
if (noMutate) {
css = `[${attr}="${CSS.escape(rawCss)}"]`;
} else {
css = `[${setAttribute(el, rawCss)}]`;
}
pluginsPre.forEach((plugin) => (rawCss = plugin(rawCss)));
css += ` { ${rawCss} }`;
pluginsPost.forEach((plugin) => (css = plugin(css)));
if (!discreteStyles) {
style += css;
} else {
elementProcessedCss.set(el[elementIdAttribute], css);
elementIdNodes.set(el[elementIdAttribute], el);
idsToUpdate.add(el[elementIdAttribute]);
}
};
querySelectorAll(doc, `[${attr}]`).forEach(processEl);
emitStyle();
observer.observe(doc.documentElement, {
attributes: true,
attributeFilter: [attr],
childList: true,
subtree: true,
});
};
xstyle.pre = pluginsPre;
xstyle.post = pluginsPost;
xstyle.version = "0.0.3";
window.xstyle = xstyle;
})();
(()=>{var e=document,t=(e,t)=>e.querySelectorAll(t),r=[],a=[],s=(s,n,o)=>{var i=[],l=0,c=`${s}-match`,d=`${s}-id`,f=0,v=new Map,u=new Map,m=new Map,p=new Map,h=new Map,b=new Set,M=new Set,g=!1;o&&setInterval((()=>v.clear()),1e4);var w=new MutationObserver((e=>{for(var r of e)if("attributes"===r.type)E(r.target);else if("childList"===r.type){for(var a of r.addedNodes)a instanceof HTMLElement&&(a.hasAttribute(s)&&E(a),[...t(a,`[${s}]`)].forEach(E));if(o)for(var a of r.removedNodes){if(!(a instanceof HTMLElement))continue;var n=a[d];if(!n)continue;m.delete(n),p.delete(n),b.delete(n);const e=h.get(n);e&&(h.delete(n),M.add(e))}}$()})),$=()=>{if(o){const s=Array.from(b.values());b.clear();for(const n of s){var t=m.get(n);if(t){var r=h.get(n);if(r)r.innerHTML=t,g||(console.log({processedCssSize:v.size}),console.log({allProcessedCssSize:u.size}),g=!0,requestAnimationFrame((()=>{const e=Array.from(M.values());M.clear(),g=!1;for(const t of e)t.remove()})));else{var a=e.createElement("style");a.innerHTML=m.get(n),e.head.appendChild(a),r=h.set(n,a)}}}}else i.length&&((a=e.createElement("style")).innerHTML=i,e.head.appendChild(a),a=null,i="")},A=(e,t)=>{var r=`${c}-${v.get(t)}`,a="__"+c;return e[a]&&e.removeAttribute(e[a]),e.setAttribute(r,""),e[a]=r,r},E=e=>{var t,c=e.getAttribute(s);e[d]=e[d]||++f,o&&e.setAttribute(d,e[d]),c&&!v.has(c)?(v.set(c,++l),u.set(c,l),t=n?`[${s}="${CSS.escape(c)}"]`:`[${A(e,c)}]`,r.forEach((e=>c=e(c))),t+=` { ${c} }`,a.forEach((e=>t=e(t))),o?(m.set(e[d],t),p.set(e[d],e),b.add(e[d])):i+=t):n||A(e,c)};t(e,`[${s}]`).forEach(E),$(),w.observe(e.documentElement,{attributes:!0,attributeFilter:[s],childList:!0,subtree:!0})};s.pre=r,s.post=a,s.version="0.0.3",window.xstyle=s})();
When you have time, can you setup a contributing guide? If you're amenable, I'd like to send in a PR for discrete
styletags for components. But I don't want to sacrifice the performance in terms of payload size and speed you're shooting for.I ask because I like this method. I like it a lot. The downside I can see, though, when trying to utilize this with any sort of long-lived client session, is that styles are smashed together into a single style head. Later updates add a new style head. With long-lived clients and frequent CSS mutations, you can find yourself with hundreds, sometimes thousands of DOM nodes whose CSS contents are later overridden (and overridden
n-1times, wherenis the number of unique props you send tox-style.(Another issue is that there is the potential for a flash for unstyled content. But that doesn't seem to happen for me on WebKit, but YMMV.)
I have a patch that adds a new configuration property (and should leave the rest alone) that enables discrete style elements in the
headfor each DOM element. Is this a good idea? I don't know, TBH. But it makes it easier for this library to manage the lifecycle of individual DOM nodes and which styles we need to keep in the DOM.In lieu of contributing directly, I mess with a lot of side projects that are UI-only SPAs (all data is stored in the browser). So I took x-style and made it do what I needed in long-lived browser client world. Take these ideas/this code directly if you want! It's gratis. Or throw it away 😄
Caveat emptor: I wrote this for me, and without robust testing, I'm hesitant to add it to this straight to the repo. In addition, I'm not sure if my cleanup method or use of browser
Setobjects is a good idea for performance. I'm also not super sure if my test is an accurate reflection of real-world performance issues. I might have just spent an hour on this for no real reason 🤷 (It was fun, tho!)I have not done any golfing with this version. A run through a minifier gets it to about 1.67kb ungzipped, which is 896 bytes gzipped. (Compared to before: 895/564, so about a 332 byte increase (~60%). This is fine for me, but might be a bit too much for this library (unless the minifier I used ain't so hot).
Updated version with DOM style tag management
gzipped version:
Here's the (React) test bed I used to play around with performance when adding, mutating, and removing lots of dom nodes
Note: the nature of this test means that you will almost certainly crash your browser eventually. My goal was to have x-style not be the cause.