Skip to content

Commit efbb1a7

Browse files
DylanPierceyclaude
authored andcommitted
Optimize class attributes built from CSS module classes
A `class` value assembled from CSS module classes (a stylesheet import or a `<style/name>` object) previously bailed to the runtime `classValue` walker, since `styles.foo` is a member expression the constant folder can't resolve. Recognize these as known class strings so the optimizer can treat them like static tokens: HTML concatenates them directly, DOM applies constant classes once at mount and toggles the conditional ones in place, and `_attr_class_item` handles a class that resolves to multiple tokens (eg from `composes:`). In development a class that resolves to a non-string logs a warning; the wrapper is compiled away in optimized builds.
1 parent 98ea020 commit efbb1a7

24 files changed

Lines changed: 542 additions & 57 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@marko/runtime-tags": patch
3+
---
4+
5+
Optimize `class` attributes built from CSS module classes. A member access on a CSS module - a default/namespace import from a stylesheet, or a `<style/name>` tag's object - is now treated as a known class string, so `class=[styles.card, { [styles.active]: on }]` no longer falls back to the runtime `class` walker. Constant classes are folded into the concatenated string on the server and applied once at mount on the client, while toggles update in place. `_attr_class_item` now also handles a class that resolves to multiple space-separated tokens (eg from `composes:`). In development, a CSS module class that resolves to a non-string (a typo or a class missing from the stylesheet) logs a warning; this assertion is compiled away in optimized builds.

.sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
{
88
"name": "*",
99
"total": {
10-
"min": 25867,
11-
"brotli": 9506
10+
"min": 25968,
11+
"brotli": 9546
1212
}
1313
},
1414
{

.sizes/dom.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// size: 25867 (min) 9506 (brotli)
1+
// size: 25968 (min) 9546 (brotli)
22
//#region packages/runtime-tags/dist/dom.mjs
33
let empty = [],
44
rest = Symbol(),
@@ -327,6 +327,9 @@ function _call(fn, v) {
327327
function stringifyClassObject(name, value) {
328328
return value ? name : "";
329329
}
330+
function _class_module_value(value, name) {
331+
return value;
332+
}
330333
function stringifyStyleObject(name, value) {
331334
return value || value === 0 ? name + ":" + value : "";
332335
}
@@ -1449,7 +1452,10 @@ function _attr_class_items(element, items) {
14491452
for (let key in items) _attr_class_item(element, key, items[key]);
14501453
}
14511454
function _attr_class_item(element, name, value) {
1452-
element.classList.toggle(name, !!value);
1455+
let { classList } = element;
1456+
if (~name.indexOf(" "))
1457+
for (let token of name.split(" ")) classList.toggle(token, !!value);
1458+
else classList.toggle(name, !!value);
14531459
}
14541460
function _attr_style(element, value) {
14551461
setAttribute(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// template.marko
2+
const $template = "<div></div><div></div>";
3+
const $walks = " b b";
4+
function $setup($scope) {
5+
_attr_class_item($scope["#div/0"], _class_module_value(void 0, "card"), 1);
6+
_attr_class_item($scope["#div/1"], _class_module_value(void 0, "card"), 1);
7+
}
8+
const $active = /* @__PURE__ */ _const("active", ($scope) => {
9+
_attr_class_item($scope["#div/0"], _class_module_value(void 0, "on"), $scope.active);
10+
_attr_class_item($scope["#div/1"], _class_module_value(void 0, "on"), $scope.active);
11+
});
12+
const $input = ($scope, input) => $active($scope, input.active);
13+
var template_default = /* @__PURE__ */ _template("__tests__/template.marko", $template, $walks, $setup, $input);
14+
15+
// v:template.marko.module.css
16+
var v_template_marko_module_default = "\n .card { color: green }\n .on { color: blue }\n";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// v:template.marko.module.css
2+
var v_template_marko_module_default = "\n .card { color: green }\n .on { color: blue }\n";
3+
4+
// template.marko
5+
var template_default = _template("__tests__/template.marko", (input) => {
6+
const $scope0_reason = _scope_reason(), $sg__input_active = _serialize_guard($scope0_reason, 0);
7+
const $scope0_id = _scope_id();
8+
const { active } = input;
9+
_html(`<div${_attr_class(`${_class_module_value(void 0, "card")}${active ? " " + _class_module_value(void 0, "on") : ""}`)}></div>${_el_resume($scope0_id, "#div/0", $sg__input_active)}<div${_attr_class(`${_class_module_value(void 0, "card")}${active ? " " + _class_module_value(void 0, "on") : ""}`)}></div>${_el_resume($scope0_id, "#div/1", $sg__input_active)}`);
10+
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {}, "__tests__/template.marko", 0);
11+
}, 1);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// template.marko
2+
var template_default = _template("a", (input) => {
3+
const $scope0_reason = _scope_reason(), $sg__input_active = _serialize_guard($scope0_reason, 0);
4+
const $scope0_id = _scope_id();
5+
const { active } = input;
6+
_html(`<div${_attr_class(`${void 0}${active ? " " + void 0 : ""}`)}></div>${_el_resume($scope0_id, "a", $sg__input_active)}<div${_attr_class(`${void 0}${active ? " " + void 0 : ""}`)}></div>${_el_resume($scope0_id, "b", $sg__input_active)}`);
7+
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {});
8+
}, 1);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"dom": {}
3+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<style/styles>
2+
.card { color: green }
3+
.on { color: blue }
4+
</style>
5+
6+
<const/{ active }=input/>
7+
8+
// A `<style/var>` object resolves classes the same way a module import does.
9+
<div class=[styles.card, { [styles.on]: active }]/>
10+
<div class=[styles.card, active && styles.on]/>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { TestConfig } from "../../main.test";
2+
3+
// CSS module class values are supplied by the bundler, not this harness (which
4+
// loads `.css` as text), so this fixture asserts on the compiled output only.
5+
export const config: TestConfig = {
6+
skip_csr: true,
7+
skip_ssr: true,
8+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// template.marko
2+
const $template = "<div></div><div></div><div></div><div></div><div class=base></div>";
3+
const $walks = " b b b b b";
4+
function $setup($scope) {
5+
_attr_class_item($scope["#div/0"], _class_module_value(styles_module_default.card, "card"), 1);
6+
_attr_class_item($scope["#div/3"], _class_module_value(styles_module_default.card, "card"), 1);
7+
_attr_class_item($scope["#div/4"], _class_module_value(styles_module_default.card, "card"), 1);
8+
}
9+
const $active = /* @__PURE__ */ _const("active", ($scope) => {
10+
_attr_class_item($scope["#div/0"], _class_module_value(styles_module_default.on, "on"), $scope.active);
11+
_attr_class_item($scope["#div/1"], _class_module_value(styles_module_default.on, "on"), $scope.active);
12+
_attr_class_item($scope["#div/3"], _class_module_value(styles_module_default.on, "on"), $scope.active);
13+
_attr_class_item($scope["#div/4"], _class_module_value(styles_module_default.on, "on"), $scope.active);
14+
});
15+
const $input_x__OR__input_y = /* @__PURE__ */ _or(10, ($scope) => _attr_class_items($scope["#div/2"], {
16+
[_class_module_value(styles_module_default.a, "a")]: $scope.x,
17+
[_class_module_value(styles_module_default.b, "b")]: $scope.y
18+
}));
19+
const $x = /* @__PURE__ */ _const("x", $input_x__OR__input_y);
20+
const $y = /* @__PURE__ */ _const("y", $input_x__OR__input_y);
21+
const $input = ($scope, input) => {
22+
$active($scope, input.active);
23+
$x($scope, input.x);
24+
$y($scope, input.y);
25+
};
26+
var template_default = /* @__PURE__ */ _template("__tests__/template.marko", $template, $walks, $setup, $input);
27+
28+
// styles.module.css
29+
var styles_module_default = ".card {\n color: green;\n}\n.on {\n color: blue;\n}\n.a {\n color: red;\n}\n.b {\n color: teal;\n}\n";

0 commit comments

Comments
 (0)