nice-html: a fast and fancy HTML generation library

Posted on January 11, 2018 by Mike Ledger

I’ve been working on and off on a HTML templating library for a few months. It was originally intended for use in Respecify, but that evolved into a single-page application. Thus a library for fast server-side HTML generation wasn’t necessary any more. (Though you could just as well use nice-html on a frontend through GHCJS.)

The biggest difference to BlazeHtml and Lucid and is that templates are compiled, so as much HTML is “rendered” (i.e., as many strings are concatenated as possible ☺) ahead-of-time as possible. This isn’t an extremely clever optimisation, but it can make a pretty big difference in render times.

To make it possible to still “inject” data into a template, a type parameter is given to the markup (compiled FastMarkup and non-compiled Markup ) that gives templates “holes”. e.g., a template that requires the description of a person to render could be typed tpl :: FastMarkup (Person -> Text) . On the other hand, a template with absolutely no dynamic data can be typed tpl :: forall a. FastMarkup a — or just tpl :: FastMarkup Void .

There are a few functions for rendering templates; take your pick. The easiest-to-use, and most magical, is r :: Render a m => a -> m Builder . This allows you to render any FastMarkup , constraining m to be a ReaderT monad when the parameter to FastMarkup has an arrow. A simpler and less magical alternative renderM :: Monad m => (a -> m Builder) -> FastMarkup a -> m Builder is also provided.

That is, given a simple template like:

Rendering can be achieved with:

The biggest drawback of this approach is that it’s more difficult to write templates in this style, despite whatever benefits it confers. The primitives that you need to be aware of for inserting “dynamic” data are:

dynamic :: p -> Markup p () inserts a hole that will be escaped. dynamicRaw :: p -> Markup p () inserts a hole that won’t be escaped, e.g. for a chunk of HTML. stream :: Foldable f => Markup (a -> n) r -> Markup (f a -> FastMarkup n) r inserts a hole for any old Foldable – e.g. [Text] , [Article] , Vector TodoItem etc. This is admittedly a bit of a misnomer, since most sane Foldable s won’t ever actually stream. Except for String s produced by Prelude.readFile , Prelude.getContents , etc., but these functions are increasingly taboo. sub :: Markup n a -> Markup (FastMarkup n) a puts templates in your templates.

A complete example

Performance

These benchmarks derive from blaze-markup ’s “bigtable” benchmark; but aren’t exactly the same. They’ve been shamelessly altered – I added some static markup to the front and end of the templates, since that’s more realistic than a table on its own – to highlight the strengths of nice-html .

The benchmark itself generates a table of N rows, and measures the time taken to render, as well as the amount of memory used, using the venerable criterion and the underrated weigh . The nice-html version looks (and by looks, I mean “is”) like:

Runtime

Abridged: blaze is fast; lucid is faster; nice-html is fasterer.

Benchmark perf: RUNNING... benchmarking 10/blaze time 91.73 μs (91.10 μs .. 92.33 μs) 1.000 R² (0.999 R² .. 1.000 R²) mean 92.64 μs (92.25 μs .. 93.03 μs) std dev 1.358 μs (1.073 μs .. 1.807 μs) benchmarking 10/nice time 35.76 μs (35.52 μs .. 36.00 μs) 1.000 R² (0.999 R² .. 1.000 R²) mean 35.50 μs (35.28 μs .. 35.67 μs) std dev 626.9 ns (467.4 ns .. 811.9 ns) variance introduced by outliers: 14% (moderately inflated) benchmarking 10/lucid time 57.08 μs (56.91 μs .. 57.27 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 57.20 μs (56.94 μs .. 57.36 μs) std dev 711.5 ns (531.2 ns .. 1.126 μs) benchmarking 100/blaze time 762.7 μs (760.5 μs .. 764.2 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 762.0 μs (759.5 μs .. 763.9 μs) std dev 7.546 μs (5.949 μs .. 9.589 μs) benchmarking 100/nice time 344.2 μs (342.9 μs .. 345.4 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 343.5 μs (342.4 μs .. 344.5 μs) std dev 3.498 μs (2.939 μs .. 4.304 μs) benchmarking 100/lucid time 486.5 μs (485.2 μs .. 487.8 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 485.5 μs (483.9 μs .. 486.6 μs) std dev 4.137 μs (2.838 μs .. 7.064 μs) benchmarking 1000/blaze time 7.243 ms (7.183 ms .. 7.310 ms) 0.999 R² (0.998 R² .. 1.000 R²) mean 7.298 ms (7.246 ms .. 7.347 ms) std dev 147.5 μs (125.5 μs .. 178.1 μs) benchmarking 1000/nice time 3.422 ms (3.387 ms .. 3.465 ms) 0.999 R² (0.999 R² .. 1.000 R²) mean 3.420 ms (3.402 ms .. 3.436 ms) std dev 56.16 μs (46.34 μs .. 69.55 μs) benchmarking 1000/lucid time 4.689 ms (4.661 ms .. 4.714 ms) 1.000 R² (1.000 R² .. 1.000 R²) mean 4.685 ms (4.667 ms .. 4.698 ms) std dev 48.05 μs (38.33 μs .. 62.37 μs) Benchmark perf: FINISH

Memory use, including compilation overhead

Benchmark mem: RUNNING... Case Allocated GCs 10/blaze 597,808 1 10/nice 3,062,248 5 10/lucid 247,008 0 100/blaze 4,556,200 8 100/nice 5,716,888 11 100/lucid 1,735,160 3 1000/blaze 44,138,200 85 1000/nice 32,264,800 62 1000/lucid 16,582,944 29 Benchmark mem: FINISH

Environment info

packages pulled from Stackage’s lts-8.13 resolver.

resolver. nice-html-0.3.0

lucid-2.9.8.1

blaze-html-0.8.1.3 and blaze-markup-0.7.1.1

Roadmap

A more honestly streaming stream using e.g. streaming or pipes or conduit – or maybe all 3 at once, just to stick it to the zealots of each ☺ – shouldn’t be too hard to implement. Rewrite Text.Html.Nice.Writer to just use a plain-old State or Writer monad internally. Have a virtual-DOM-esque rerender function that (somehow) only re-renders what is likely (i.e, if a parameter has changed) to change, and some JavaScript glue code to enable a client to replace “old” HTML. I’ve scratched at the surface of this with note , which just gives nodes a unique id attribute, but it would be really neat to achieve this – my eventual dream is to be able to write fast server-side single-page-applications (especially if updates are facilitated over e.g. a WebSocket) entirely in Haskell without needing GHCJS. jsaddle might already do this but I haven’t seriously looked into it.

Hackage GitHub type-of-html is another fairly recent library that “compiles” HTML (and is a Haskell EDSL), but it does so at the type-level.

Enjoy!

Please enable JavaScript to view the comments powered by Disqus.

Disqus