Skip to content

re-frame.performance

Measure how long events, subscriptions, effects and view renders take in a production build. With timing on, re-frame2 records each one as a User Timing measure, so browser DevTools, an APM or any PerformanceObserver can read it. This is separate from the trace stream, which exists only in development builds: in development, Xray and the trace stream already time each run and also say why it ran, so reach for this when you need timings from the build you ship.

Application code never calls this namespace. Timing is controlled by two compile-time flags, named fully qualified in your build's :closure-defines. enabled? turns it on:

;; shadow-cljs.edn
{:builds {:app {:compiler-options
                {:closure-defines {re-frame.performance/enabled? true}}}}}
  • Each measure is named rf:<kind>:<id>: rf:event:<event-id> for an event handler, rf:sub:<query-id> for a subscription recompute (a cached read records nothing), rf:fx:<fx-id> for an effect handler, or rf:render:<view-id> for a render of a view registered with reg-view or defined with Fresco's defview. A keyword id is written without its leading colon, as in rf:event:todo/add. No performance.mark entries are created.
  • CLJS only. On the JVM both flags are constant false and timing does nothing, because the Performance API exists only in the browser.

To send the rf: measures to an APM, forward them from a PerformanceObserver:

(def timing-observer
  (js/PerformanceObserver.
    (fn [entries _]
      (doseq [e (.getEntries entries)
              :when (.startsWith (.-name e) "rf:")]
        (js/console.log (.-name e) (.-duration e))))))  ;; or send to your APM

(.observe timing-observer #js {:type "measure"})
;; At teardown, after collecting the measurements you need:
;; (.disconnect timing-observer)

Find and fix a slow view § Only slow in production shows the measures in the DevTools Performance panel and in an APM.

Compile-time flags

enabled?

  • Kind: var (compile-time goog-define boolean)
  • Signature: set with :closure-defines {re-frame.performance/enabled? true} (CLJS). On the JVM it is ^:const false.
  • Description: Turns on the timing measures for event handling, subscription recomputes, fx handlers and view renders. Default false.
    • Each measure is emitted as performance.measure(name, {start, end}), with numeric performance.now() timestamps.
    • The measure is emitted in a try/finally, so it is recorded even when the measured code throws; the exception still propagates.
    • Unless retain-entries? is on, the entry is cleared by name (performance.clearMeasures) straight after it is emitted. An attached PerformanceObserver has its own queue and receives the entry asynchronously; clearing the timeline buffer does not clear that queue.
    • Attach the observer before the work you want to measure. With the default retention setting, attaching later with buffered: true cannot recover entries already cleared from the timeline.
    • It is read at compile time only: it is not a rf/configure! option, and changing it at runtime has no effect. With the default, every measure site is removed under :advanced.

retain-entries?

  • Kind: var (compile-time goog-define boolean)
  • Signature: set with :closure-defines {re-frame.performance/retain-entries? true} (CLJS). On the JVM it is a constant false.
  • Description: Keeps measure entries in the browser's User Timing buffer instead of clearing each one after it is emitted. Default false.
    • Turn it on for one-shot reads with performance.getEntriesByType("measure") in the console. A DevTools Performance recording does not need it.
    • Leave it off for long-running sessions such as real-user monitoring. The browser's measure buffer has no size limit, so retained entries accumulate for the life of the page. With it off, each entry is delivered to any live PerformanceObserver and cleared, so the buffer does not grow and getEntriesByType returns no rf:* entries.
    • It has no effect unless enabled? is also on.
    • Like enabled?, it is read at compile time only; changing it at runtime has no effect.
  • Example:
    ;; shadow-cljs.edn — retain entries for one-shot DevTools / console reads.
    ;; Leave off for long-running sessions; read via a PerformanceObserver.
    {:builds {:app {:compiler-options
                    {:closure-defines {re-frame.performance/enabled?        true
                                       re-frame.performance/retain-entries? true}}}}}
    

Framework integration

Not for application code. A view substrate uses these so that the name it publishes for a view is the name that view's measure carries.

entry-id

  • Kind: function
  • Signature: (entry-id id) → string
  • Description: The <id> half of a measure name: a keyword without its leading colon, anything else as written. A substrate stamps it as the component name it publishes, such as React's displayName, so the name DevTools shows matches the rf:render: measure.

build-name

  • Kind: function
  • Signature: (build-name kind id) → string
  • Description: The whole measure name, rf:<kind>:<id>, whose <id> is (entry-id id).

mark-and-measure

  • Kind: macro
  • Signature: (mark-and-measure kind id body+) → the last body value
  • Description: With enabled? on, records body as one rf:<kind>:<id> measure, even when it throws; with enabled? off, an :advanced build keeps only body.

See also

  • Configure dev and prod — how the timing flag combines with goog.DEBUG across build profiles.
  • Observability — how production timing sits alongside the trace and error surfaces.