Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/onionpancakes/chassis

Fast HTML5 serialization for Clojure
https://github.com/onionpancakes/chassis

clojure hiccup html serialization template

Last synced: about 24 hours ago
JSON representation

Fast HTML5 serialization for Clojure

Awesome Lists containing this project

README

        

# Chassis

Fast HTML5 serialization for Clojure.

Renders [Hiccup](https://github.com/weavejester/hiccup/) style HTML vectors to strings.

Highly optimized runtime serialization without macros. Even faster serialization when combined with compiling macros.

* See [Compiling Elements](#compiling-elements).
* See [Performance](#performance).

# Status

[![Run tests](https://github.com/onionpancakes/chassis/actions/workflows/run_tests.yml/badge.svg)](https://github.com/onionpancakes/chassis/actions/workflows/run_tests.yml)

Production released.

# Deps

Add one of these deployments to `deps.edn`.

### GitHub

```clojure
dev.onionpancakes/chassis {:git/url "https://github.com/onionpancakes/chassis"
:git/tag "v1.0.365" :git/sha "3e98fdc"}
```

### Clojars

```clojure
dev.onionpancakes/chassis {:mvn/version "1.0.365"}
```

# Example

### Runtime HTML Serialization

```clojure
(require '[dev.onionpancakes.chassis.core :as c])

(defn my-post
[post]
[:div {:id (:id post)}
[:h2.title (:title post)]
[:p.content (:content post)]])

(defn my-blog
[data]
[c/doctype-html5 ; Raw string for
[:html
[:head
[:link {:href "/css/styles.css" :rel "stylesheet"}]
[:title "My Blog"]]
[:body
[:h1 "My Blog"]
(for [p (:posts data)]
(my-post p))]]])

(let [data {:posts [{:id "1" :title "foo" :content "bar"}]}]
(c/html (my-blog data)))

;; "My Blog

My Blog


foo


bar


"
```

### Compiled HTML Serialization

```clojure
(require '[dev.onionpancakes.chassis.core :as c])
(require '[dev.onionpancakes.chassis.compiler :as cc])

(defn my-post-compiled
[post]
(cc/compile
[:div {:id (:id post)}
[:h2.title (:title post)]
[:p.content (:content post)]]))

(defn my-blog-compiled
[data]
(cc/compile
[c/doctype-html5 ; Raw string for
[:html
[:head
[:link {:href "/css/styles.css" :rel "stylesheet"}]
[:title "My Blog"]]
[:body
[:h1 "My Blog"]
(for [p (:posts data)]
(my-post-compiled p))]]]))

(let [data {:posts [{:id "1" :title "foo" :content "bar"}]}]
(c/html (my-blog-compiled data)))

;; "My Blog

My Blog


foo


bar


"
```

# Usage

Require the namespace.

```clojure
(require '[dev.onionpancakes.chassis.core :as c])
```

## Elements

Use `c/html` function to generate HTML strings from vectors.

Vectors with **global keywords** in the head position are treated as normal HTML elements. The keyword's name is used as the element's tag name.

```clojure
(c/html [:div "foo"])

;; "

foo
"
```

Maps in the second position are treated as attributes. Use **global keywords** to name attribute keys.

```clojure
(c/html [:div {:id "my-id"} "foo"])

;; "

foo
"
```

```clojure
;; Strings also accepted, but discouraged.
;; Use when keywords cannot encode the desired attribute name.
(c/html [:div {"id" "my-id"} "foo"])

;; "

foo
"
```

The rest of the vector is treated as the element's content. They may be of any type including other elements. Sequences, eductions, and [non-element vectors](#non-element-vectors) are logically flattened with the rest of the content.

```clojure
(c/html [:div {:id "my-id"}
"foo"
(for [i (range 3)] i)
"bar"])

;; "

foo012bar
"
```

## Id and Class Sugar

Like Hiccup, id and class attributes can be specified along with the tag name using css style `#` and `.` syntax.

```clojure
(c/html [:div#my-id.my-class "foo"])

;; "

foo
"
```

```clojure
;; Multiple '.' classes concatenates
(c/html [:div.my-class-1.my-class-2 "foo"])

;; "

foo
"
```

```clojure
;; '.' classes concatenates with :class keyword
(c/html [:div.my-class-1 {:class "my-class-2"} "foo"])

;; "

foo
"
```

```clojure
;; First '#' determines the id.
;; Extra '#' are uninterpreted.
(c/html [:div## "foo"])

;; "

foo
"

(c/html [:div#my-id.my-class-1#not-id "foo"])

;; "

foo
"
```

However there are differences from Hiccup.

```clojure
;; '#' id takes precedence over :id keyword
(c/html [:div#my-id {:id "not-my-id"} "foo"])

;; "

foo
"
```

```clojure
;; '#' id can be place anywhere
(c/html [:div.my-class-1#my-id "foo"])

;; "

foo
"
```

```clojure
;; '#' id can be place in-between, but don't do this.
;; It will be slightly slower.
(c/html [:div.my-class-1#my-id.my-class-2 "foo"])

;; "

foo
"
```

## Boolean Attributes

Use `true`/`false` to toggle boolean attributes.

```clojure
(c/html [:button {:disabled true} "Submit"])

;; "Submit"

(c/html [:button {:disabled false} "Submit"])

;; "Submit"
```

## Composite Attribute Values

Collections of attribute values are concatenated as spaced strings.

```clojure
(c/html [:div {:class ["foo" "bar"]}])

;; "

"

(c/html [:div {:class #{:foo :bar}}])

;; "

"
```

Maps of attribute values are concatenated as style strings.

```clojure
(c/html [:div {:style {:color :red
:border "1px solid black"}}])

;; "

"
```

Attribute collections and maps arbitrarily nest.

```clojure
(c/html [:div {:style {:color :red
:border [:1px :solid :black]}}])

;; "

"
```

## Write to Appendable

Avoid intermediate allocation by writing directly to [`java.lang.Appendable`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Appendable.html) using the `c/write-html` function.

However, [`java.lang.StringBuilder`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringBuilder.html) is highly optimized and it may be faster to write to it (and then write the string out) than to write to the Appendable directly. Performance testing is advised.

```clojure
(let [out (get-appendable-from-somewhere)]
(c/write-html out [:div "foo"]))
```

## Escapes

Text and attribute values are escaped by default.

```clojure
(c/html [:div "& < >"])

;; "

& < >
"

(c/html [:div {:foo "& < > \" '"}])

;; "

"
```

Escaping can be disabled locally by wrapping string values with `c/raw`.

```clojure
(c/html [:div (c/raw "

foo

")])

;; "

foo

"
```

Escaping can be disabled globally by altering vars. Change `c/escape-text-fragment` and `c/escape-attribute-value-fragment` to
`identity` function to allow fragment values to pass through unescaped.

Then use `c/escape-text` and `c/escape-attribute-value` to escape locally.

```clojure
(alter-var-root #'c/escape-text-fragment (constantly identity))
(alter-var-root #'c/escape-attribute-value-fragment (constantly identity))

(c/html [:div "

foo

"])

;; "

foo

"

(c/html [:div (c/escape-text "foo & bar")])

;; "

foo & bar
"
```

### Vetted Unescaped Types

For performance, `java.lang.Number` and `java.util.UUID` are not escaped by default.

### Tags and Attribute Keys Are Not Escaped!

Element tags and attribute keys are not escaped. Be careful when placing dangerous text in these positions.

```clojure
;; uhoh
(c/html [:<> "This is bad!"])

;; "<<>>This is bad!<>>"

(c/html [:div {:<> "This is bad!"}])

;; "

=\"This is bad!\">
"
```

## Non-Element Vectors

Only vectors beginning with keywords are interpreted as elements. Vectors can set their metadata `{::c/content true}` to avoid being interpreted as elements, even if they begin with keywords.

```clojure
;; Not elements
(c/html [0 1 2]) ; => "012"
(c/html ["foo" "bar"]) ; => "foobar"
(c/html ^::c/content [:foo :bar]) ; => "foobar"

;; Use this to generate fragments of elements
(c/html [[:div "foo"]
[:div "bar"]]) ; "

foo
bar
"
```

## Non-Attribute Keys

Only **global keywords** and **strings** are interpreted as attribute keys. Everything else is ignored.

```clojure
(c/html [:div {:foo/bar "not here!"}])

;; "

"
```

## Alias Elements

Alias elements are user defined elements. They resolve to other elements through the `c/resolve-alias` multimethod. They must begin with **namespaced keywords**.

Define alias elements by extending `c/resolve-alias` multimethod on a namespaced keyword. It accepts the following 3 arguments of types:

1. Tag keyword. Used for the dispatch.
2. Attributes map or nil if attrs is absent.
3. Content vector, possibly empty if no content.

When implementing aliases, consider the following points:

* Because namespaced keywords are ignored as attributes, they can be used as arguments for alias elements.

* The attributes map will contain `#id` and `.class` merged from the element tag. By placing the alias element's attribute map as the attribute map of a resolved element, the attributes transfers seamlessly between the two.
* The content vector has metadata `{::c/content true}` to avoid being interpreted as an element.

```clojure
;; Capitalized name optional, just to make it distinctive.
(defmethod c/resolve-alias ::Layout
[_ {:layout/keys [title] :as attrs} content]
[:div.layout attrs ; Merge attributes
[:h1 title]
[:main content]
[:footer "Some footer message."]])

(c/html [::Layout#blog.dark {:layout/title "My title!"}
[:p "My content!"]])

;; "


My title!


My content!

Some footer message.
"
```

## Stateful Values

Instances of `clojure.lang.IDeref` and `clojure.lang.Fn` are automatically dereferenced at serialization. Functions are invoked on their zero argument arity.

Whether or not if this is a good idea is left to the user.

```clojure
(defn current-year []
(.getValue (java.time.Year/now)))

(c/html [:footer "My Company Inc " current-year])

;; "My Company Inc 2024"
```

```clojure
(def delayed-thing
(delay "delayed"))

(c/html [:div {:foo delayed-thing}])

;; "

"
```

They can even deference into other elements.

```clojure
(defn get-children []
[:p "Child element"])

(c/html [:div.parent get-children])

;; "

Child element

"
```

## Token and HTML Serializers

Use `c/token-serializer` and `c/html-serializer` to access individual tokens and fragment instances. The underlying type implements `clojure.lang.IReduceInit` and is intended to be used in a reduce.

```clojure
(->> (c/token-serializer [:div "foo"])
(eduction (map type))
(vec))

;; [dev.onionpancakes.chassis.core.OpeningTag
;; java.lang.String
;; dev.onionpancakes.chassis.core.ClosingTag]
```

```clojure
(->> (c/html-serializer [:div "foo"])
(vec))

;; ["

" "foo" "
"]
```

## RawString Constants

### DOCTYPE

Use `c/doctype-html5`, a `RawString` wrapping ``. Because it is a `RawString`, it is safe to wrap in a vector to concatenate with the rest of the HTML document.

```clojure
(c/html [c/doctype-html5 [:html "..."]])

;; "..."
```

### &nbsp;

Use the `c/nbsp` constant.

```clojure
(c/html [:div "foo" c/nbsp "bar"])

;; "

foo bar
"
```

# Compiling Elements

Require the namespace.

```clojure
(require '[dev.onionpancakes.chassis.compiler :as cc])
```

## Compile Examples

Slap a `cc/compile` wherever speed is needed! Then call `c/html` like normal to generate HTML.

```clojure
;; In defs
(def global-element
(cc/compile [:div "foo"]))

;; In defns
(defn fn-element
[arg]
(cc/compile [:div "foo" arg "bar"]))

;; In aliases
(defmethod c/resolve-alias ::MyElement
[_ attrs content]
(cc/compile
[:div
[:p attrs content]]))

;; In fn args
(fn-element (cc/compile [:p "some content"]))

;; Then call c/html like normal to generate HTML.
(c/html (fn-element 123))

;; "

foo123bar
"
```

## Compile Usage

Chassis provides compiling macros `cc/compile` and `cc/compile*`. They take **one** argument, the root HTML tree, and they return compiled versions of the HTML tree. Use them to compile elements and pass their results to `c/html`.

```clojure
(defn my-element []
(cc/compile
[:div [:p "foobar"]]))

(c/html (my-element))

;; "

foobar

"
```

Compiling **flattens** and **compacts** the HTML tree, making subsequent calls to `c/html` much faster.

```clojure
(macroexpand-1 '(cc/compile [:div [:p "foobar"]]))

;; Results in:
#object[dev.onionpancakes.chassis.core.RawString 0x11c2d9a2 "

foobar

"]

(let [body (identity "some-dynamic-content")]
(pprint
(macroexpand-1
'(cc/compile
[:div.deeply
[:div.nested
[:div.thing
[:p "before" body "after"]]]]))))

;; Results in:
[#object[dev.onionpancakes.chassis.core.RawString 0x66fd28ce "

before"]
body
#object[dev.onionpancakes.chassis.core.RawString 0xe9c5af6 "after

"]]
```

Use `cc/compile` for most purposes. For performance, the returned value may or may not be a vector. This is so that compiling small fragments of fully compacted HTML (like `


`) is as efficient as possible when iterated over by `c/html`.

```clojure
;;


is not wrapped as a 1-sized vector
(cc/compile [:hr])

;; #object[dev.onionpancakes.chassis.core.RawString 0x6ba58490 "


"]

;; The end result is the same either way,
;; but the runtime serialization is faster this way.
(->> (range 10)
(interpose (cc/compile [:hr]))
(c/html))

;; "0


1
2
3
4
5
6
7
8
9"
```

Use `cc/compile*` to ensure the return value is a vector. Otherwise, it is the same as `cc/compile`.

```clojure
;;


is wrapped as a 1-sized vector
(cc/compile* [:hr])

;; [#object[dev.onionpancakes.chassis.core.RawString 0x24f1caeb "


"]]
```

### Compiled Elements Must Have Literal Tags

A small but subtle difference between `cc/compile` and `c/html` is that `cc/compile` assumes elements are **literal** vectors with **literal** keyword tags. Vectors without literal tags, after [var resolution](#var-resolved-constants), are assumed to be content.

```clojure
;; Basically don't do this.
(let [footag :div]
(c/html (cc/compile [footag "It's foobarred."])))

;; "divIt's foobarred."

;; Works at runtime.
(let [footag :div]
(c/html [footag "It's foobarred."]))

;; "

It's foobarred.
"
```

## Ambiguous Attributes Produce Speed Bumps

Ambiguous objects in the second position forces the compiler to emit checks which examine the potential attributes map at runtime.

```clojure
(let [data {:body "foo"}]
(pprint (clojure.walk/macroexpand-all
;; Compiler can't see what (:body data) returns.
'(cc/compile [:div (:body data)]))))

;; Results in:
[(let*
[attrs13712 (:body data)]
(if ;; Check if 2nd item is attrs map at runtime.
(dev.onionpancakes.chassis.core/attrs? attrs13712)
(dev.onionpancakes.chassis.core/->OpeningTag
nil
:div
nil
nil
attrs13712)
[#object[dev.onionpancakes.chassis.core.RawString 0x1cc8f6fb "

"]
attrs13712]))
#object[dev.onionpancakes.chassis.core.RawString 0x6753cbe6 "
"]]
```

### Resolving Ambiguity - Force Attributes Absence

Use `nil` in second position to force compile the element without attributes.

```clojure
(let [data {:body "foo"}]
(pprint (macroexpand-1
'(cc/compile [:div nil (:body data)]))))

;; Results in:
[#object[dev.onionpancakes.chassis.core.RawString 0x6e42ae2e "

"]
(:body data)
#object[dev.onionpancakes.chassis.core.RawString 0x588c9f7d "
"]]
```

### Resolving Ambiguity - Force Attributes Presence

Type hint the second position with either `java.util.Map` or `clojure.lang.IPersistentMap` to force compile elements with attributes.

```clojure
(let [data {:attrs {:foo "bar"}
:body "foo"}]
(pprint (macroexpand-1
'(cc/compile [:div ^java.util.Map (:attrs data) (:body data)]))))

;; Results in:
[(dev.onionpancakes.chassis.core/->OpeningTag
nil
:div
nil
nil
(:attrs data))
(:body data)
#object[dev.onionpancakes.chassis.core.RawString 0x6314faa ""]]
```

Type hinting the argument or bindings also works.
* Note: It doesn't show up correctly in a `macroexpand`, but it does works normally. This is because `cc/compile` examines the type hints from macro implied arg `&env`, and `macroexpand` for some reason doesn't capture `&env`.

```clojure
;; Should work!
(defmethod c/resolve-alias ::CompileWithAttrs
[_ ^java.util.Map attrs content]
(cc/compile [:div attrs content]))

(let [^java.util.Map attrs {:foo "bar"}]
(cc/compile [:div attrs "foobar"]))
```

### Vetted Attributes Core Functions

Certain functions in `clojure.core` which returns maps are considered as attributes when called in the second position. Type hinting these invocations is not necessary. They include:

* `array-map`
* `hash-map`
* `sorted-map`
* `sorted-map-by`
* `assoc`
* `assoc-in`
* `merge`
* `select-keys`
* `update-keys`
* `update-vals`

```clojure
;; Useful in aliases when merging attrs.
(defmethod c/resolve-alias ::AliasWithAttrsMerge
[_ attrs content]
(cc/compile
[:div (merge {:foo "bar"} attrs)
content]))
```

### Warn on Ambiguous Attributes

Call `(cc/set-warn-on-ambig-attrs!)` to turn on warnings when compiling elements with ambiguous attributes. It will add a tap which prints out warning messages to `*err*` whenever ambiguous attributes are compiled.

Call `(cc/unset-warn-on-ambig-attrs!)` to disable.

## Compilation Barriers

### Function Calls

Functions calls, and generally any list values, block compilation traversal. Call `cc/compile` again to compile forms within.

```clojure
(defn comp-blocked
[]
[:p "blocked"])

(cc/compile [:div "foo" (comp-blocked) "bar"])

;; Results in:
[#object[dev.onionpancakes.chassis.core.RawString 0x67574bda "

foo"]
[:p "blocked"]
#object[dev.onionpancakes.chassis.core.RawString 0x565edf06 "bar
"]]
```

### Alias Elements

Alias elements are implemented as `c/resolve-alias` (via `c/resolve-alias-with-meta`) function calls. As a result, they also block compilation. However, the arguments passed to `c/resolve-alias` will be compiled.

```clojure
(defmethod c/resolve-alias ::CompileMyAlias
[_ attrs content]
[:div attrs content])

(pprint
(clojure.walk/macroexpand-all
'(cc/compile
[::CompileMyAlias {:foo "bar"}
[:p "content 1"]
[:p "content 2"]])))

;; Results in:
(dev.onionpancakes.chassis.core/resolve-alias-with-meta
nil
:user/CompileMyAlias
{:foo "bar"}
[#object[dev.onionpancakes.chassis.core.RawString 0x34e3a7d6 "

content 1

content 2

"]])
```

### Macro Calls

Macros are expanded during compilation. Like function calls, those which expand into lists block compilation.

```clojure
(pprint
(cc/compile
[:ol
(for [i (range 4)]
[:li i])]))

;; Results in:
[[#object[dev.onionpancakes.chassis.core.OpeningTag 0x6e462cc4 "

    "]
    ([:li 0] [:li 1] [:li 2] [:li 3])]
    #object[dev.onionpancakes.chassis.core.RawString 0x27b55932 "
"]]

;; Manually call compile in the inner form to reach inside.
(pprint
(cc/compile
[:ol
(for [i (range 4)]
(cc/compile [:li i]))]))
```

Macros which expand into non-lists can participate in compilation. Therefore, it is possible to use macros to abstract element components in a compile friendly way.

Whether or not if this is a good idea is left to the user.

```clojure
(defmacro NonBlockingElement
[content]
[:p nil content])

(cc/compile [:div (NonBlockingElement "not-blocked")])

;; Results in:
#object[dev.onionpancakes.chassis.core.RawString 0x31b2d0a8 "

not-blocked

"]
```

## Var Resolved Constants

Symbols referring to **vars** containing **constant values** are **resolved** to those values during compilation traversal, thereby allowing those constant values to participate in compilation. Constant types include `String`, `Long`, `IPersistentCollection` of constants, and `RawString` such as `c/doctype-html5` and `c/nbsp`. Use `cc/constant?` to check if values are constants.

```clojure
;; Fully compacted!
;; Even with a symbol splitting content in the middle.
(cc/compile [:div "foo" c/nbsp "bar"])

;; Results in:
#object[dev.onionpancakes.chassis.core.RawString 0x7fb21735 "

foo bar
"]
```

## Runtime Compilation

Chassis provides two analogous compile functions, `cc/compile-node` and `cc/compile-node*`, for compiling HTML tree at runtime. They are useful for compiling static HTML pages or components.

Because compiling happens at runtime, lists, function calls, and alias elements are no longer compilation barriers and ambiguous attributes are not possible.

Runtime compilation is similar to generating HTML with `c/html` but with key differences:

* The return values are `c/raw` strings, allowing the result to be embedded in other HTML components without the HTML tags being escaped.
* Stateful values, such as functions and derefs, are not realized.

```clojure
(defn current-time []
(java.time.LocalTime/now))

(defmethod c/resolve-alias ::CurrentTime
[_ _ _]
[:p "Current time is: " current-time])

(def static-page
(cc/compile-node
[::CurrentTime]))

;; Results in:
[#object[dev.onionpancakes.chassis.core.RawString 0x7a702aaf "

Current time is: "]
;; Notice current-time function is not yet called.
#object[user$current_time 0x584d9dc4 "user$current_time@584d9dc4"]
#object[dev.onionpancakes.chassis.core.RawString 0x1c59c510 "

"]]

;; Stateful values realized on call to c/html
(c/html static-page)

;; "

Current time is: 13:48:14.228299269

"
```

# Performance

At this time, benchmarks shows Chassis to be 2x faster (and often more!) when compared to other Clojure HTML templating libraries on equivalent benchmark examples.

See bench results in the resource folder.

```clojure
$ clj -M:dev
Clojure 1.11.1

;; Chassis

user=> (quick-bench (chassis-page data-mid))
Evaluation count : 2712 in 6 samples of 452 calls.
Execution time mean : 229.730870 µs
Execution time std-deviation : 7.583674 µs
Execution time lower quantile : 221.593639 µs ( 2.5%)
Execution time upper quantile : 237.951723 µs (97.5%)
Overhead used : 8.800684 ns
nil
user=> (quick-bench (chassis-page-compiled data-mid))
Evaluation count : 4722 in 6 samples of 787 calls.
Execution time mean : 131.554387 µs
Execution time std-deviation : 4.400562 µs
Execution time lower quantile : 127.024648 µs ( 2.5%)
Execution time upper quantile : 137.206151 µs (97.5%)
Overhead used : 8.800684 ns
nil
user=> (quick-bench (chassis-page-compiled-unambig data-mid))
Evaluation count : 6186 in 6 samples of 1031 calls.
Execution time mean : 100.309952 µs
Execution time std-deviation : 3.392984 µs
Execution time lower quantile : 98.074419 µs ( 2.5%)
Execution time upper quantile : 105.031335 µs (97.5%)
Overhead used : 8.800684 ns
nil

;; Hiccup

user=> (quick-bench (hiccup-page data-mid))
Evaluation count : 990 in 6 samples of 165 calls.
Execution time mean : 615.536499 µs
Execution time std-deviation : 15.886454 µs
Execution time lower quantile : 599.567903 µs ( 2.5%)
Execution time upper quantile : 637.703394 µs (97.5%)
Overhead used : 8.800684 ns
nil
user=> (quick-bench (hiccup-page-compiled data-mid))
Evaluation count : 1044 in 6 samples of 174 calls.
Execution time mean : 594.160734 µs
Execution time std-deviation : 15.249740 µs
Execution time lower quantile : 576.246477 µs ( 2.5%)
Execution time upper quantile : 611.946104 µs (97.5%)
Overhead used : 8.800684 ns
nil
user=> (quick-bench (hiccup-page-compiled-unambig data-mid))
Evaluation count : 2544 in 6 samples of 424 calls.
Execution time mean : 246.390352 µs
Execution time std-deviation : 6.001164 µs
Execution time lower quantile : 240.872342 µs ( 2.5%)
Execution time upper quantile : 255.422063 µs (97.5%)
Overhead used : 8.800684 ns
nil

;; Selmer

user=> (quick-bench (selmer-page data-mid))
Evaluation count : 1428 in 6 samples of 238 calls.
Execution time mean : 455.954085 µs
Execution time std-deviation : 14.867158 µs
Execution time lower quantile : 443.374807 µs ( 2.5%)
Execution time upper quantile : 478.302764 µs (97.5%)
Overhead used : 8.800684 ns
nil

;; Enlive

user=> (quick-bench (enlive-page-item-html data-mid))
Evaluation count : 282 in 6 samples of 47 calls.
Execution time mean : 2.254892 ms
Execution time std-deviation : 83.779038 µs
Execution time lower quantile : 2.156587 ms ( 2.5%)
Execution time upper quantile : 2.341325 ms (97.5%)
Overhead used : 8.800684 ns
nil
```

## Element Vector Allocation is Small

Element vector allocation accounts for a small % of the runtime cost.

```clojure
user=> (quick-bench (page-doall data-mid))
Evaluation count : 34752 in 6 samples of 5792 calls.
Execution time mean : 18.073864 µs
Execution time std-deviation : 623.107379 ns
Execution time lower quantile : 17.421242 µs ( 2.5%)
Execution time upper quantile : 18.715025 µs (97.5%)
Overhead used : 8.800684 ns
nil
```

The vast proportion of the runtime cost is the iteration of HTML data structure and fragment writes.

### It's All Interned

Keywords and Strings are interned objects. Therefore the cost of allocating HTML vectors is mostly the cost of allocation vectors, and allocating vectors is really fast.

# License

Released under the MIT License.