Internals
Minification
Bascik features zero-dependency minifiers for HTML, CSS, and JavaScript, deterministic identifier hashing for scoped selectors, and custom minifier extensibility.
Overview
Minification reduces payload sizes without introducing heavy external bundlers or AST parsers. Bascik includes three specialized minification passes:
html-minifier.ts: Strips comments, consolidates eligible scripts, and collapses whitespace, deciding from a spec-following scan of the document (html-scanner.ts) what each part of the markup is.css-minifier.ts: Removes comments and structural whitespace while shielding string literals andurl()definitions.js-minifier.ts: Strips comments and unnecessary spaces while preserving string literals, template literals, and regex literals verbatim.- Identifier Hashing (
names.ts): Hashes scoped class names and element IDs using SHA-256 and Base62 encoding whenminify.identifiers: trueis configured. - BYO Minifier: Supports custom third-party minifier integrations (
esbuild,SWC,PostCSS,Lightning CSS) configured inbascik.config.ts.
Zero-AST Lexical Context Preservation
Traditional build tools rely on heavy Abstract Syntax Tree (AST) parsers to minify code safely. Bascik achieves equivalent safety and higher throughput using zero-dependency lexical context preservation:
- HTML Tokenizer Scan:
html-scanner.tswalks the document once, following the WHATWG HTML tokenizer states, and splits it into text, tags, raw text bodies, and other markup. Comment-like text inside a<script>or<style>, a quoted>in an attribute value, and malformed markup such as<scr<script>are all read the way a browser reads them, so they are never mistaken for document structure. - CSS String and Resource Shielding: Quoted string literals and
url(...)declarations in CSS can contain colons, semicolons, or multiple spaces (such as data URIs or content strings).shieldCssStringsextracts these values into temporary tokens before structural whitespace stripping, restoring them unchanged afterward. - JS Lexical Context and Regex Disambiguation: JavaScript code is segmented into literal regions (quoted strings, template literals, regexes) and minifiable code regions. To disambiguate the forward slash
/character (which can represent either a division operator or a regex literal),js-minifier.tstracks preceding keyword context (such asreturn,case,typeof,yield,await). Forward slashes following expression keywords are preserved as regex literals.
HTML Minification (html-minifier.ts)
minifyHtml makes two linear passes, each driven by the scanner:
- Scan the document, remove its comments, and take out the client scripts selected for consolidation.
- Scan the result and collapse whitespace token by token, then place the consolidated scripts.
The goal is that the minified page parses to the same document, with the same scripts able to run. The test suite checks this against parse5, a spec-compliant HTML parser, on random malformed markup.
Key HTML Minification Behaviors
- Comment Stripping: Every HTML comment the browser would parse is removed, including ones ending in
--!>, except inside<pre>and<listing>, whose content stays verbatim. Comment-like text inside raw text and RCDATA elements (<script>,<style>,<textarea>,<title>,<noscript>) is not a comment and is left in place, so CSS CDO/CDC tokens (<!--and-->) inside a<style>element survive. Removing markup never joins the text around it into new markup: where it could (<then<!-- c -->script>, or&am<!-- -->p;), an empty<!---->takes its place. - Verbatim Content:
<pre>and<listing>content, raw text and RCDATA bodies (<script>,<style>,<textarea>,<xmp>,<noscript>,<iframe>), SVG and MathML<script>and<style>, attribute values, DOCTYPEs, and CDATA sections are never changed. Whitespace between a tag's attributes collapses, since it never renders.<title>text collapses, becausedocument.titlecollapses it anyway. - Whitespace Collapsing: In other text, every line break and every run of spaces, tabs, and line breaks becomes one space. Only that whitespace is touched: U+00A0 (
written as a character), other Unicode spaces, and form feeds are content and stay. Whitespace-only text between two tags is removed, or kept as one space when both are elements the browser displays inline by default:INLINE_TAGS(a,span,b,strong,code,textarea,ins,svg,math,video,iframe, ...), any custom element (a name with a hyphen), and the SVG text content elements<tspan>,<textPath>, and<tref>, so</tspan> <tspan>inside an SVG<text>keeps its space. A comment renders nothing, so the whitespace around an<!---->a table keeps where a comment or script stood still separates two inline elements: one space stays, after the last comment. While a formatting element such as<a>or<b>may be open, it is kept as one space instead, because whitespace makes the parser re-create a misnested formatting element. - Script Consolidation: Eligible classic and module client scripts are moved to the end of the document, in their original order. Build, routes, server, and data scripts stay in place, as do scripts in
<template>,<pre>, SVG, or MathML. A script that must run in place (an import map, or one of those) keeps every earlier script before it. When the document ends inside markup where an appended script would not run, such as an unclosed tag, comment,<template>,<textarea>, or SVG, the scripts go just before it. An unclosed<svg>or<math>that a later<p>,<div>, or<span>closes, as the browser does, is not such markup, so the scripts still go at the end. Inside<foreignObject>or<mi>, the scan follows the HTML elements that decide whether</svg>or</math>closes it, as the browser builds them: a<div>or heading closes an open<p>first, and a stray<td>,<tr>,<head>, or<body>with no table open opens nothing.
Known HTML Limits
- CSS-dependent whitespace: The minifier does not read your CSS. An element other than
<pre>or<listing>styledwhite-space: pre,pre-wrap,pre-line, orbreak-spacesloses its extra spaces and line breaks, and an element made inline with CSS can lose the space next to it. Use<pre>for preformatted text, or setminify.html: false. - Malformed markup the scan cannot follow: In a few rare constructs, such as raw text or SVG elements inside
<select>, an end tag other than</option>or</optgroup>inside a<select>in SVG or MathML,<frameset>, CDATA inside SVG<title>, SVG and MathML end tags that close elements outside them, or a table tag such as<tr>or</table>inside SVG or MathML that opened in a table (the browser may leave the SVG or MathML there, depending on where in the table it is), the scan stops and the rest of the document is left as written. Inside<pre>after a<table>or<select>, it keeps treating the content as preformatted until the next<template>boundary, which only keeps more whitespace.
JavaScript minification applies to scripts with no type and to text/javascript, module, application/javascript, text/ecmascript, and application/ecmascript. External src scripts and non-JavaScript data scripts are not minified.
CSS Minification (css-minifier.ts)
minifyCss reduces stylesheet sizes by stripping comments and collapsing whitespace around CSS syntax delimiters ({, }, :, ;, ,).
export const minifyCss = (css: string): string => {
const { css: shielded, restore } = shieldCssStrings(
removeCommentsFromCss(css),
);
const minified = shielded
.replace(/\n/g, " ")
.replace(/\s\s+/g, " ")
// Preserve the single space in an empty custom property (`--empty: ;`).
.replace(/(?<!-)--([\w-]+)\s*:\s+(?=[;}])/g, "--$1: ")
.replace(/\s*([{};,])\s*/g, "$1")
.replace(/:\s+/g, ":")
// A space before `:` is a descendant combinator in a selector
// (`.a :hover`), so strip it only in a declaration.
.replace(/\s+:(?![^;{}]*\{)/g, ":")
.replace(/(?<!-)--([\w-]+):(?=[;}])/g, "--$1: ")
.trim();
return restore(minified);
}; String and URL Shielding (shieldCssStrings)
CSS property values can contain strings, data URIs, or custom properties containing colons, semicolons, or multiple spaces:
.badge::after {
content: "Status: Active; Version 1.0";
}
.hero {
background-image: url("data:image/svg+xml;charset=utf-8,...");
} shieldCssStrings extracts quoted string literals and url(...) declarations before minification runs, replacing them with temporary tokens. After structural whitespace is stripped, restore() re-injects the original string content unchanged.
JavaScript Minification (js-minifier.ts)
minifyJs performs single-pass lexical scanning to strip comments and collapse whitespace in client-side scripts.
Lexical Segmenting
JavaScript source code is divided into literal segments (which must be preserved verbatim) and minifiable code segments:
type Segment = {
literal: boolean;
text: string;
controlHeaderCloses?: Set<number>; // `)` closing an if/while/for/with header
postfixUpdateEnds?: Set<number>; // end of each postfix `++`/`--`
arrowBodyCloses?: Set<number>; // `}` closing an arrow function body (`=> {...}`)
asiBefore?: boolean; // literal only: a line break before it ends the statement
}; Quoted strings ("...", '...'), template literals (`...`), and regex literals (/.../) are placed in literal segments. A hashbang (#!) first line is kept as written, with its line break, and only the rest of the script is minified.
Regex Disambiguation
A forward slash / can denote either a division operator or the start of a regular expression literal. To disambiguate, js-minifier.ts tracks preceding keyword context:
const REGEX_PRECEDING_KEYWORDS = new Set([
"return",
"case",
"throw",
"yield",
"await",
"delete",
"typeof",
"void",
"default",
"in",
"of",
"instanceof",
"new",
"do",
"else",
]); When / follows an operator or one of these keywords, it is parsed as a regular expression literal and preserved intact. A / after a value, ], a quote, a ) that does not close an if/while/for/with header, or a postfix ++/-- (count++ / 2) is division. After } the scanner tries a regex, because a block can end there, and keeps it only when it closes on the same line. js-literal-spans.ts, which finds the string literals that attribute scoping reads, imports this keyword set but reads a / after } or any ) as division.
Line Breaks (ASI)
Collapsing a line break must keep the statement boundary that JavaScript's automatic semicolon insertion puts there, so the minified script runs the same program as the unminified one. The scanner records what it needs for this on each segment (controlHeaderCloses, postfixUpdateEnds, arrowBodyCloses, asiBefore). The rules are listed in the "Semicolon-free scripts in minify.js" row of the compatibility page.
Identifier Hashing (names.ts)
When production identifier minification is enabled (minify.identifiers: true), Bascik replaces long scoped class names and element IDs with compressed alphanumeric hashes.
Base62 Hash Encoding
names.ts computes SHA-256 digests of scoped attribute names, and the portable name-hash.ts (shared with request-time rendering on Workers) encodes the first 64 bits into a Base62 string:
const BASE62_ALPHABET =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const toBase62 = (num: bigint, length = 11): string => {
if (num === 0n) return "0".repeat(length);
let str = "";
let current = num;
while (current > 0n) {
const remainder = Number(current % 62n);
str = BASE62_ALPHABET[remainder] + str;
current = current / 62n;
}
return str.padStart(length, "0");
};
export const nameHashFromDigest = (digest: Uint8Array): string => {
let num = 0n;
for (let i = 0; i < 8; i++) num = (num << 8n) | BigInt(digest[i]);
return `b${toBase62(num, 11)}`;
};
// names.ts
export const getAttributeNameHash = (attributeName: string): string => {
const digest = createHash("sha256").update(attributeName).digest();
return nameHashFromDigest(digest); // memoized in hashCache
}; The output is prefixed with a b character to ensure class and ID names always begin with a valid CSS letter identifier rather than a digit (for example, b2Y4G9eD1K8b).
O(1) Hash Memoization (hashCache)
To eliminate redundant crypto and string allocations during builds, getAttributeNameHash caches computed Base62 hashes in an in-memory Map<string, string> (hashCache). Subsequent encounters of identical attribute names across pages or components return the cached hash instantly with 0 SHA-256 digests, BigInt conversions, or string creation overhead.
Bring Your Own Minifier (BYO Minifier)
For projects with specialized optimization needs, Bascik allows overriding the default minifiers in bascik.config.ts. Custom minifiers can be synchronous or asynchronous.
Integration Examples
JavaScript with esbuild
import { transform } from "esbuild";
export default {
minify: {
js: async (code: string) => {
const result = await transform(code, { loader: "js", minify: true });
return result.code.trim();
},
},
}; CSS with Lightning CSS or PostCSS
import postcss from "postcss";
import autoprefixer from "autoprefixer";
export default {
minify: {
css: async (code: string) => {
const result = await postcss([autoprefixer]).process(code, {
from: undefined,
});
return result.css;
},
},
}; When custom minifier functions are provided, Bascik routes compiled assets through the custom handlers during the final build phase.