Sept. 10, 2023

Today I put together a small test repo to check how much space is saved when replacing React with Preact in a ClojureScript project.

I used npm init shadowfront prtest to get a basic project up and running. This creates a simple one page Reagent app with a button you can click.

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

(defonce state (r/atom {}))

(defn component-main [_state]
  [:div
   [:h1 "prtest"]
   [:p "Welcome to the app!"]
   [:button {:on-click #(js/alert "Hello world!")}
            "click me"]])

(defn start {:dev/after-load true} []
  (rdom/render [component-main state]
               (js/document.getElementById "app")))

(defn init []
  (start))

I made a build to check the size of the resulting js binary. Then I uninstalled react and react-dom and installed preact@8 and preact-compat. Then I updated shadow-cljs.edn to add the following clause into the :app build:

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

This asks shadow-cljs to alias those React modules to the Preact compatibility layer throughout the whole stack.

I ran make to build the project before and after the change and got the following results:

  1. With React = 292k
    $ du -hs build/js/main.js 
    292K    build/js/main.js
  1. With Preact = 172k
    $ du -hs build/js/main.js 
    172K    build/js/main.js

A 41% size reduction (120k) for a simple one page app seems pretty good. Most of the remaining 172k would be ClojureScript core and libraries such as Reagent.

Update: shadow-cljs lets us generate a build report. Here's a build report with React and then Preact:

React ClojureScript build report

Package Weight %
react-dom @ npm: 18.2.0 128.22 KB 45.8 %
org.clojure/clojurescript @ mvn: 1.11.60 115.71 KB 41.4 %
reagent @ mvn: 1.1.0 22.93 KB 8.2 %
react @ npm: 18.2.0 6.49 KB 2.3 %
scheduler @ npm: 0.23.0 3.96 KB 1.4 %
org.clojure/google-closure-library @ mvn: 0.0-20230227-c7c0a541 1.12 KB 0.4 %
Generated Files 932 0.3 %
src 490 0.2 %

Preact ClojureScript build report

Package Weight %
org.clojure/clojurescript @ mvn: 1.11.60 115.61 KB 71.4 %
reagent @ mvn: 1.1.0 22.94 KB 14.2 %
preact-compat @ npm: 3.19.0 9.24 KB 5.7 %
preact @ npm: 8.5.3 8.18 KB 5.1 %
preact-context @ npm: 1.1.4 2.55 KB 1.6 %
org.clojure/google-closure-library @ mvn: 0.0-20230227-c7c0a541 1.12 KB 0.7 %
Generated Files 931 0.6 %
prop-types @ npm: 15.8.1 801 0.5 %
src 490 0.3 %

June 23, 2023

Hello! Today I am very excited to announce a thing I've been tinkering with for the past month or so. You can find it at https://livereload.net.

It's a simple online utility that enables live reloading web development for your local HTML/JS/CSS projects. It's easy to use and you don't have to install anything. Just drag your web project folder onto the window and your index page will show up. When you edit the files on your local machine they will live-reload in the browser and you'll see your changes immediately.

That is basically all there is to it. I've found live reloading to be so useful in my own development and I wanted to make it easy for anybody to get this feature without complicated command line build tooling. I discovered a browser filesystem feature that allows polling for file changes and realized I could use it for this, and so I did.

So there you have it. If you have any feedback do let me know. Enjoy!

screenshot.png

March 26, 2023

tl;dr: you can generate very small (less than 1k) JS artifacts from ClojureScript with some tradeoffs. I worked out a list of rules to follow and made the cljs-ultralight library to help with this.

Photograph of a glider in the air

Most of the web apps I build are rich front-end UIs with a lot of interactivity. Quite often they are generating audio in real time and performing other complicated multimedia activites. This is where ClojureScript and shadow-cljs really shine. All of the leverage of a powerful LISP with its many developer-friendly affordances (editor integration, hot-loading, interop, repl) brought to bear, allowing me to quickly iterate and build with fewer bugs.

On many projects I find myself also needing a small amount of JavaScript on a mostly static page. An example would be a static content page that has a form with a submit button that needs to be disabled until particular fields are filled. It seems a bit excessive to send a 100s of kilobyte JS file with the full power of Clojure's immutable datastructures and other language features just to change an attribute on one button.

In the past I resorted a tiny bit of vanilla JS to solve this problem. I have now discovered I can use ClojureScript carefully to get most of what is nice about the Clojure developer experience and still get a very small JS artifact.

Here's an example from the Jsfxr Pro accounts page. What this code does is check whether the user has changed a checkbox on the accounts page, and shows the "save" (submit) button if there are any changes.

(ns sfxrpro.ui.account)

(defn start {:dev/after-load true} []
  (let [input (.querySelector js/document "input#update-email")
        submit-button (.querySelector js/document "button[type='submit']")
        initial-value (-> input .-checked)]
    (aset submit-button "style" "display" "none")
    (aset input "onchange"
          (fn [ev]
            (let [checked (-> ev .-target .-checked)]
              (aset submit-button "style" "display"
                    (if (coercive-= checked initial-value)
                      "none"
                      "block")))))))

(defn main! []
  (start))

This code compiled to around 500 bytes. It has since been updated to do a bunch of different more complicated stuff and today it compiles to 900 bytes. I'll talk about some of the special weirdness and language tradeoffs in a second, but first here is the shadow-cljs config I used.

{:builds {:app {:target :browser
                :output-dir "public/js"
                :asset-path "/js"
                :modules {:main {:init-fn sfxrpro.ui/main!}}
                :devtools {:watch-dir "public"}
                :release {:output-dir "build/public/js"}}
          :ui {:target :browser
               :output-dir "public/js"
               :asset-path "/js"
               :modules {:account {:init-fn sfxrpro.ui.account/main!}}
               :devtools {:watch-dir "public"}
               :release {:output-dir "build/public/js"}}}

The first build target :app is for the main feature-rich app which does all the complicated stuff. I am fine with this being a larger artifact as it does a lot of things for the user including realtime generation of audio samples.

The second target :ui creates a file called account.js which is just 900 bytes. It gets loaded on the accounts page which is statically rendered. The reason for two completely separate build targets is otherwise the compiler will smoosh all of the code together and bloat your artifact size. It is easiest just to keep both codebases completely separate.

When compiling I found it useful to have a terminal open watching the file size of account.js so I could see real time when the size ballooned out and figure out which code was making that happen.

So what tricks do we have to use in the code to get the artifact size down? Here is a brief list of rules to follow to stay small. If you break any of these rules your artifact size will balloon.

  1. Do not use any native Clojure data types. Don't use vec or hash-map or list for example. Instead you have to use native JavaScript data structures at all times like #js {:question 42} and #js [1 2 3]. That also means you will have to use aget and aset instead of get and assoc. It means we are dropping immutablity and other data type features.
  2. Do not use Clojure's = operator. I know that sounds mad but what you can use instead is ClojureScript's coercive-=. This function does a native-style surface level JavaScript comparison. This means you have to give up the value based equality comparison you can use on deeply nested datastructures in Clojure.
  3. Do not use certain built-ins like partial. Other clever built-ins like juxt are probably going to be bad for file size too. As far as I can tell it's anything that uses immutable Clojure types under the hood. For the specific case of partial you can use #(...) instead to do what you need.
  4. Use js/console.log instead of print.
  5. Use (.map some-js-array some-func) instead of (map some-func some-js-array)

Generally as far as possible you should stick with native JavaScript calls and data types.

If all of this sounds onerous remember that the idea here is to only do this in situations where you have a small codebase giving the user some small amount of interactivity on a web page. So that's the tradeoff. You still get LISP syntax, editor integration, hot-loading, repl, and lots of other nice Cloure stuff, but you have to forgo immutable datastructures and language features like partial.

I have created a small library called cljs-ultralight to help with the UI side of things. It uses browser calls and returns JS data types. You can use it to perform common UI operations like attaching event handlers and selecting elements, without incurring too much overhead.

The library applied-science.js-interop also works with these techniques. Require it like this: [applied-science.js-interop :as j] and you can use j/assoc! and j/get and friends. Note if you use j/get-in or other functions that take a list argument, you should instead pass a JavaScript array which works well.

Also note that @borkdude has a couple of very interesting projects under way in this space. Check out squint and cherry for more details.

March 2, 2023

be288dc6b22d9de42a1c2d2c4c47cb10.png

Hello! Just in time for 7DRL, Roguelike Browser Boilerplate is now open source.

The boilerplate is a JavaScript based game template that takes care of all the annoying stuff like splash screen, start screen, credits screen, instructions screen, settings screen, menus, pixel styled UI, win/lose condition screens, sound effects, animations, etc. so you can get on with making the actual game.

It's ROT.js based and includes example implementations for monster, inventory, level gen, etc. It works on mobile and desktop.

The license is MIT so you can do what you want basically, including using it in a commercial game.

Enjoy!

Feb. 10, 2023

I predict Python will be the most used programming language among developers world wide by 2032. This post contains my reasoning.

First take a look at this chart.

Chart of most used programming langauges 2022 from Statista

(Chart source. In the chart Javascript is #1 with 65%, HTML/CSS is #2 with 55%, SQL is #3 with 49%, Python is #4 with 48%.)

It looks this way because of the web. The world has chosen web tech over everything else because it is fast and easy to make software that runs on the web. It is fast and easy to let other people run your software on the web. You don't need anybody's permission. The only developer dependency you really need is a web browser, and the web browser is a feature rich runtime environment that can be programmed to do a huge variety of user-facing multi-media things.

The reason why Python is fourth on that list is because it is the easiest thing to use for just about everything else. From data science to web backends to ops glue code and micro-controller snippets. If Python ran in the browser it would probably be the easiest thing to use there too. There have been several attempts to bring Python into the browser. PyScript, based on Pyodide, is the latest and the best so far.

I took it for a spin the other day. It is quite large (900k) and it is quite a bit slower to load than native JS. It does this annoying thing of hard-coding a loading spinner instead of letting the developer take care of that. Clearly though, this iteration of Python in the browser is now good enough for a large number of front end use cases. Here is my minimal hello world PyScript HTML file to get you started:

<!doctype html>
<html lang="en-us">
  <head>
    <title>PyScript test</title>
    <meta charset="utf-8">
    <script defer src="https://pyscript.net/latest/pyscript.js"></script> 
    <style>
      body { max-width: 800px; width: 100%; margin: 1em auto; font-size: 2em; }
      #pyscript_loading_splash { display: none; }
      py-script { display: none; }
    </style>
  </head>
  <body>
    <div id="app">Loading...</div>
  </body>
  <py-script>
    from js import alert
    Element("app").write('PyScript was here!')
    # alert("hi")
  </py-script>
</html>

If you have a Python heavy dev team and you're building some internal web tool that doesn't have to load instantly, it's a no-brainer. Even if you're building a larger public facing app, it's probably good enough. Lots of front end apps today are this heavy and load about as slowly anyway. I have no doubt the size and speed will improve over time.

I think a lot of people will start using Python in front end browser code soon. They like Python already, and now they can use it in the web browser. It will only take a large-ish minority of projects using Python to bump it up above JavaScript, since it is already so popular in other domains. I expect that will happen in the not too distract future.

I don't think JavaScript will go away. It is the most popular programming language in the world and the syntax is not that much less accessible than Python. I think both languages will remain popular, and hopefully many other languages too, but I do think Python will become the most popular language.

Here are my own browser dev preferences in case you are interested. I no longer write as much Python or JavaScript code as I use full-stack ClojureScript. I like JavaScript and I am grateful that Eich was there at the right time and place to create this amazing LISP in C's clothing. The web would not be the same without the swiss army knife network language that is JS. I enjoy Python too. It is a very accessible language and that is an important reason why it is so popular.

Note, I have used the word "easy" here on purpose. Simple is not always easy, and simple is probably more important than easy, but the world sure does love easy.