Shadow-cljs Example: Replacing React with Preact for Smaller Bundle Size

A while back I came across Preact, which advertises itself as "Fast 3kB alternative to React with the same modern API". I thought I'd try it out with ClojureScript, shadow-cljs and reagent.

Let's start out with shadow-cljs browser quickstart template. After cloning the repo, we need to install preact:

$ npm install preact

In shadow-cljs.edn inside our build we need to add

:js-options
 {:resolve
  {"react" {:target :npm :require "preact/compat"}
   "react-dom" {:target :npm :require "preact/compat"}}}

Adding also reagent as a dependency, the file should look like this:

{:source-paths
 ["src/dev"
  "src/main"
  "src/test"]

 :dependencies
 [[reagent "1.2.0"]]

 :dev-http
 {8020 "public"}

 :builds
 {:app
  {:target :browser
   :output-dir "public/js"
   :asset-path "/js"
   :js-options
   {:resolve
    {"react" {:target :npm :require "preact/compat"}
     "react-dom" {:target :npm :require "preact/compat"}}}

   :modules
   {:main ; becomes public/js/main.js
    {:init-fn starter.browser/init}}}}}

Then we can just npx shadow-cljs watch app and open it in browser. Checking the console we can see the messages from js/console.log statements as defined in browser.cljs.

Let's add some minimal functionality to browser.cljs to see that reagent still works:

(ns starter.browser
  (:require [reagent.core :as r]
            [reagent.dom :as rdom]))

(defn component-main []
  (let [state (r/atom {})]
    (fn []
      [:div
       [:input
        {:type "text"
         :on-change (fn [e]
                      (reset! state (-> e .-target .-value)))}]
       [:div "Reversed: "
        (reverse @state)]])))

;; start is called by init and after code reloading finishes
(defn ^:dev/after-load start []
  (js/console.log "start")
  (rdom/render [component-main]
               (js/document.getElementById "app")))

(defn init []
  ;; init is called ONCE when the page loads
  ;; this is called in the index.html and must be exported
  ;; so it is available even in :advanced release builds
  (js/console.log "init")
  (start))

;; this is called before any code is reloaded
(defn ^:dev/before-load stop []
  (js/console.log "stop"))

You can view the full thing on github.

The resulting js-file (npx shadow-cljs release app) is roughly 182 KB. I tried the same using react and react-dom (version "17.0.2" for both) and the resulting file was 274 KB.

Note: if you're planning on migrating your current project from React to Preact make sure you go through differences to React and test properly to ensure nothing breaks.

Edit: Chris McCormick already had a post about this: Replacing React with Preact in ClojureScript. I highly recommend his writings.