Skip to content

Commit

Permalink
Optimize for the common case (#17)
Browse files Browse the repository at this point in the history
* Generate a memoized function for computing a style's name

* Use the newly-generated naming fn; improve manual :key support

* Clean up

* Prepare to only conditionally compile CSS, update DOM

* Respect global :always-compile-css?

* Add support for :always-compile-css per-function escape hatch

* Clean up some TODOs

* Change StyleContainer to support skipping compile when mounted

Now for the common case of applying a style that's already in the DOM,
all we have to do is compute its name (which is memoized on the params!)
and lookup the info to handle any composed styles. We don't run the
style factory fn and we certainly don't compile any CSS!

* Invoke style-factory directly, without `apply`

* Update core-test to remove unnecessary apply params

* Fix: find-key-meta not working consistently

* Rename vars for consistency
  • Loading branch information
dhleong committed Jul 10, 2023
1 parent f0441e2 commit 5197e54
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 94 deletions.
10 changes: 8 additions & 2 deletions src/spade/container.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
(defprotocol IStyleContainer
"The IStyleContainer represents anything that can be used by Spade to
'mount' styles for access by Spade style components."
(mounted-info
[this style-name]
"Given a style-name, return the info object that was passed when style-name
was mounted, or nil if that style is not currently mounted.")
(mount-style!
[this style-name css]
"Ensure the style with the given name and CSS is available"))
[this style-name css info]
"Ensure the style with the given name and CSS is available. [info]
should be stored somewhere in-memory to be quickly retrieved
by a call to [mounted-info]."))
11 changes: 8 additions & 3 deletions src/spade/container/alternate.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@

(deftype AlternateStyleContainer [get-preferred fallback]
IStyleContainer
(mounted-info
[_ style-name]
(or (when-let [preferred (get-preferred)]
(sc/mounted-info preferred style-name))
(sc/mounted-info fallback style-name)))
(mount-style!
[_ style-name css]
[_ style-name css info]
(or (when-let [preferred (get-preferred)]
(sc/mount-style! preferred style-name css))
(sc/mount-style! fallback style-name css))))
(sc/mount-style! preferred style-name css info))
(sc/mount-style! fallback style-name css info))))
13 changes: 10 additions & 3 deletions src/spade/container/atom.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
"The AtomStyleContainer renders styles into an atom it is provided with."
(:require [spade.container :refer [IStyleContainer]]))

(deftype AtomStyleContainer [styles-atom]
(deftype AtomStyleContainer [styles-atom info-atom]
IStyleContainer
(mount-style! [_ style-name css]
(swap! styles-atom assoc style-name css)))
(mounted-info [_ style-name]
(get @info-atom style-name))
(mount-style! [_ style-name css info]
(swap! styles-atom assoc style-name css)
(swap! info-atom assoc style-name info)))

(defn create-container
([] (create-container (atom nil)))
([styles-atom] (create-container styles-atom (atom nil)))
([styles-atom info-atom] (->AtomStyleContainer styles-atom info-atom)))
18 changes: 12 additions & 6 deletions src/spade/container/dom.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@
(defn- perform-update! [obj css]
(set! (.-innerHTML (:element obj)) css))

(defn update! [styles-container id css]
(defn update! [styles-container id css info]
(swap! styles-container update id
(fn update-injected-style [obj]
(when-not (= (:source obj) css)
(perform-update! obj css))
(assoc obj :source css))))
(assoc obj :source css :info info))))

(defn inject! [target-dom styles-container id css]
(defn inject! [target-dom styles-container id css info]
(let [element (doto (js/document.createElement "style")
(.setAttribute "spade-id" (str id)))
obj {:element element
:source css
:info info
:id id}]
(assert (some? target-dom)
"An <head> element or target DOM is required to inject the style.")
Expand All @@ -33,17 +34,22 @@

(deftype DomStyleContainer [target-dom styles]
IStyleContainer
(mount-style! [_ style-name css]
(mounted-info [_ style-name]
(let [resolved-container (or styles
*injected-styles*)]
(:info (get @resolved-container style-name))))

(mount-style! [_ style-name css info]
(let [resolved-container (or styles
*injected-styles*)]
(if (contains? @resolved-container style-name)
(update! resolved-container style-name css)
(update! resolved-container style-name css info)

(let [resolved-dom (or (when (ifn? target-dom)
(target-dom))
target-dom
(.-head js/document))]
(inject! resolved-dom resolved-container style-name css))))))
(inject! resolved-dom resolved-container style-name css info))))))

(defn create-container
"Create a DomStyleContainer. With no args, the default is created, which
Expand Down
181 changes: 115 additions & 66 deletions src/spade/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@
(:key (meta (first style))))

(defn- find-key-meta [style]
(postwalk
(fn [form]
(if (and (map? form)
(::key form))
form
(->> style

(if-let [k (:key (meta form))]
{::key k}
(postwalk
(fn [form]
(if (and (map? form)
(::key form))
form

form)))
style))
(if-let [k (:key (meta form))]
{::key k}

(if-let [k (when (sequential? form)
(some ::key form))]
{::key k}

form)))))

::key))

(def ^:private auto-imported-at-form?
#{'at-font-face
Expand Down Expand Up @@ -92,39 +99,43 @@

[nil style]))

(defn- with-composition [composition name-var style-var]
(let [base {:css `(spade.runtime/compile-css ~style-var)
:name name-var}]
(defn- with-composition [composition name-var key-var style-var]
(let [base (cond->
{:css `(when ~name-var
(spade.runtime/compile-css ~style-var))
:name name-var}

key-var (assoc ::key key-var))]
(if composition
(assoc base :composes composition)
base)))

(defn- build-style-naming-let
[style params original-style-name-var params-var]
(let [has-key-meta? (find-key-meta style)
[style params name-var]
(let [has-key-meta? (some? (find-key-meta style))
static-key (extract-key style)
name-var (gensym "name")]
key-var (gensym "key")]
(cond
; easiest case: no params? no need to call build-style-name
(nil? (seq params))
[nil original-style-name-var nil]
[nil name-var nil nil]

(or static-key
(not has-key-meta?))
; if we can extract the key statically, that's better
[nil name-var `[~name-var (#'build-style-name
~original-style-name-var
~static-key
~params-var)]]
; typical case: no custom key
(not has-key-meta?)
[nil name-var nil nil]

; okay case: a (nearly) static key that we can pull out and compute
; directly, without building the rest of the style form
static-key
[nil name-var key-var
`[~key-var ~static-key]]

; fancy case: custom :key
:else
(let [base-style-var (gensym "base-style")]
[base-style-var name-var `[~base-style-var ~(vec style)
key# (:key (meta (first ~base-style-var)))
~name-var (#'build-style-name
~original-style-name-var
key#
~params-var)]]))))
[base-style-var name-var key-var
`[~base-style-var ~(vec style)
~key-var (:key (meta (first ~base-style-var)))]]))))

(defn- prefix-at-media [style]
(postwalk
Expand All @@ -138,31 +149,32 @@
form))
style))

(defn- transform-named-style [style params style-name-var params-var]
(defn- transform-named-style [style params style-name-var]
(let [[composition style] (extract-composes style)
style-var (gensym "style")
style (->> style prefix-at-media rename-vars)
[base-style-var name-var name-let] (build-style-naming-let
style params style-name-var
params-var)
[base-style-var name-var key-var style-naming-let] (build-style-naming-let
style params style-name-var)
style-decl (if base-style-var
`(into [(str "." ~name-var)] ~base-style-var)
(into [`(str "." ~name-var)] style))]
`(let ~(vec (concat name-let
`(let ~(vec (concat style-naming-let
[style-var style-decl]))
~(with-composition composition name-var style-var))))
~(with-composition composition name-var key-var style-var))))

(defn- transform-keyframes-style [style params style-name-var params-var]
(defn- transform-keyframes-style [style params style-name-var]
(let [style (->> style prefix-at-media rename-vars)
[style-var name-var style-naming-let] (build-style-naming-let
style params style-name-var
params-var)
info-map `{:css (spade.runtime/compile-css
(garden.stylesheet/at-keyframes
~name-var
~(or style-var
(vec style))))
:name ~name-var}]
[base-style-var name-var key-var style-naming-let] (build-style-naming-let
style params style-name-var)
info-map (cond->
`{:css (when ~name-var
(spade.runtime/compile-css
(garden.stylesheet/at-keyframes
~name-var
~(or base-style-var (vec style)))))
:name ~name-var}

key-var (assoc ::key key-var))]

; this (let) might get compiled out in advanced mode anyway, but
; let's just generate simpler code instead of having a redundant
Expand All @@ -171,45 +183,71 @@
`(let ~style-naming-let ~info-map)
info-map)))

(defn- transform-style [mode style params style-name-var params-var]
(defn- transform-style [mode style params style-name-var]
(let [style (replace-at-forms style)]
(cond
(#{:global} mode)
`{:css (spade.runtime/compile-css ~(vec (rename-vars style)))
:name ~style-name-var}
`{:css (spade.runtime/compile-css ~(vec (rename-vars style)))}

; keyframes are a bit of a special case
(#{:keyframes} mode)
(transform-keyframes-style style params style-name-var params-var)
(transform-keyframes-style style params style-name-var)

:else
(transform-named-style style params style-name-var params-var))))
(transform-named-style style params style-name-var))))

(defn- generate-style-name-fn [factory-fn-name factory-name-var style params]
(cond
(empty? params)
`(clojure.core/constantly ~factory-name-var)

; Custom :key meta; we need to generate the form to extract that.
; TODO: Ideally we can invoke the factory but *skip* CSS compilation,
; but since this is memoized (and :key isn't much used anyway) this is
; probably not a big deal for now. Would be a nice optimization, however.
(some? (find-key-meta style))
`(clojure.core/memoize
(fn [params#]
(let [dry-run# (~factory-fn-name nil params#)]
(#'build-style-name
~factory-name-var
(::key dry-run#)
params#))))

:else
`(clojure.core/memoize
(partial
#'build-style-name
~factory-name-var
nil))))

(defmulti ^:private declare-style
(fn [mode _class-name params _factory-name-var _factory-fn-name]
(fn [mode _class-name params _name-fn-name _factory-fn-name]
(case mode
:global :static
(cond
(some #{'&} params) :variadic
(every? symbol? params) :default
:else :destructured))))
(defmethod declare-style :static
[mode class-name _ factory-name-var factory-fn-name]
[mode class-name _ name-fn-name factory-fn-name]
`(def ~class-name (spade.runtime/ensure-style!
~mode
~factory-name-var
(meta (var ~class-name))
~name-fn-name
~factory-fn-name
nil)))
(defmethod declare-style :no-args
[mode class-name _ factory-name-var factory-fn-name]
[mode class-name _ name-fn-name factory-fn-name]
`(defn ~class-name []
(spade.runtime/ensure-style!
~mode
~factory-name-var
(meta (var ~class-name))
~name-fn-name
~factory-fn-name
nil)))
(defmethod declare-style :destructured
[mode class-name params factory-name-var factory-fn-name]
[mode class-name params name-fn-name factory-fn-name]
; good case; since there's no variadic args, we can generate an :arglists
; meta and a simplified params list that we can forward simply
(let [raw-params (->> (range (count params))
Expand All @@ -221,27 +259,30 @@
~raw-params
(spade.runtime/ensure-style!
~mode
~factory-name-var
(meta (var ~class-name))
~name-fn-name
~factory-fn-name
~raw-params))))
(defmethod declare-style :variadic
[mode class-name _params factory-name-var factory-fn-name]
[mode class-name _params name-fn-name factory-fn-name]
; dumb case; with a variadic params vector, any :arglists we
; provide gets ignored, so we just simply collect them all
; and pass the list as-is
`(defn ~class-name [& params#]
(spade.runtime/ensure-style!
~mode
~factory-name-var
(meta (var ~class-name))
~name-fn-name
~factory-fn-name
params#)))
(defmethod declare-style :default
[mode class-name params factory-name-var factory-fn-name]
[mode class-name params name-fn-name factory-fn-name]
; best case; simple params means we can use them directly
`(defn ~class-name ~params
(spade.runtime/ensure-style!
~mode
~factory-name-var
(meta (var ~class-name))
~name-fn-name
~factory-fn-name
~params)))

Expand All @@ -250,19 +291,27 @@
(or (vector? params)
(nil? params))]}
(let [factory-fn-name (symbol (str (name class-name) "-factory$"))
style-name-fn-name (symbol (str (name class-name) "-name$"))

style-name-var (gensym "style-name")
params-var (gensym "params")
factory-params (vec (concat [style-name-var params-var] params))
factory-name-var (gensym "factory-name")]
`(do
(defn ~factory-fn-name ~factory-params
~(transform-style mode style params style-name-var params-var))
(defn ~factory-fn-name [~style-name-var ~params-var]
~(if params
`(let [~params ~params-var]
~(transform-style mode style params style-name-var))
(transform-style mode style nil style-name-var)))

(let [~factory-name-var (factory->name
(macros/case
:cljs ~factory-fn-name
:clj (var ~factory-fn-name)))]
~(declare-style mode class-name params factory-name-var factory-fn-name)))))
:clj (var ~factory-fn-name)))

~style-name-fn-name ~(generate-style-name-fn
factory-fn-name factory-name-var style params)]

~(declare-style mode class-name params style-name-fn-name factory-fn-name)))))

(defmacro defclass
"Define a CSS module function named `class-name` and accepting a vector
Expand Down
Loading

0 comments on commit 5197e54

Please sign in to comment.