Tools for Fast (and Less Furious) Frontend Development
Wednesday, February 2, 2022
You can think of a Felt map as a space which has certain Felt elements (drawings, pins, notes, data) that live inside it.
You can think of a Felt map as a space which has certain Felt elements (drawings, pins, notes, data) that live inside it.
And there are a bunch of operations that you can do to a Felt element: move it around, rotate it, change some properties on it, and so on.
Maybe that sounds obvious, but I actually think that it makes Felt very unlike a lot of software that is primarily forms and buttons and menus and listsofthings. Felt has these objects that live inside it. (Plus, a map is at least a little bit morewiggly than all those other kinds of software.)
Felt is more like an environment or a space, with its own basic building blocks that are governed by certain ’physical rules’. You might compare it to Figma [which has vector elements], or Notion [blocks], or Excel [cells], or a text editor [characters], or to your filesystem or desktop [files and folders], or even to a Web page [the HTML elements that comprise the page].
I wanted to talk about some techniques for quickly developing and iterating on this sort of (object-y, visual, direct-manipulation) software. The following is a grab bag of general techniques which have paid dividends here at Felt.
1. Hot reload!
As I think many people in software now realize, iteration speed is all-important. You want to have immediate feedback into what your changes are doing to the software – it’s great for motivation, it lets you play and try new things, and come up with solutions on the fly that you wouldn’t otherwise think of.
Hot reload is interesting in that it’s as much an aspiration as it is a particular technology–you need different technology to make different parts of your application ‘hot-reloadable’ – so you can spend an almost unbounded amount of time improving reload coverage (edits to React component source? changes to CSS? changes to map rendering code?) and improving reload speed (a minute? 10 seconds? 500 ms? 50 ms?).
At the moment, we’re at a middle point on that spectrum–we have hot reload for React components and for our Elixir backend, but some CSS top-level theme changes require a refresh, for example. We’ll probably improve on that as our styling gets more complicated and we want to tweak it more continuously.
There’s a human element to how you should set up hot reload. Is someone in the organization making a lot of a certain kind of change? Are they blocked on waiting for the thing to reload, or / could they design better with live feedback? Maybe you should fix hot reload for that area; unblocking them will probably produce a lot of value.
It’s worth noting that hot reload shouldn’t be magic or ‘set and forget’; as a developer, you generally want to know how the hot reload system works, because it will fail or go wrong at some point. You need to know what classes of things you can do ‘hot’ and what classes of things will glitch out or require a restart. A lot of that is specific to the context of your project and what kind of changes you’re typically making. In our experience, the fast feedback loop is worth this price.
2. Fast & Typeless Compilation
We write all our frontend code in TypeScript, but the hot compilation doesn’t actually use the TypeScript compiler – it runs the code through swc and doesn’t do typechecking. That means that, first of all, it’s much, much faster than using <p-inline>tsc<p-inline>. We can hot reload in a few hundred milliseconds at most (and I think we could go faster if we pushed). We also have git pre-push checks and continuous integration and editor support. This keeps us aware of type errors, allows us to check the error log as needed, but the errors don't interrupt the critical path of compilation and development work. They appear asynchronously, on the side, from the TypeScript language server, and at push time.
Equally as important, it doesn’t feel like typechecking is a blocker for development; you don’t need to care about the type errors if you’re working on something; so you can do classic dynamically-typed programming/exploration/probing /playing around early on:
I want to see what’s in this object (or if it changes over time!); I’ll assign it to <p-inline>window.foo<p-inline>, and poke at it in the browser console at runtime.
I want a bit of context/provenance when something happens later in the program, so early on, I’ll tag some data on as an extra property <p-inline>._extraStuff<p-inline> onto this object that’s getting passed through my program already.
I just want to see if this thing will work at all, so I’ll ignore the null case or error case for now (or I won’t bother to give the right type, or whatever)
You can fix up the types when you’re ready to commit/push–or when you run into a roadblock and you’re like, “okay, maybe I’ve missed something,” and you consult the type error log. The type errors are useful; don’t get me wrong! They just shouldn’t necessarily hold you up all the time or distort the way that you program.
3. Development Server Over the Internet
We’ve started using ngrok, which is a service that lets me take the https://localhost:4000 development server on my laptop and put it right up on the Internet at a URL that looks like https://21e7-19-35-39-115.ngrok.io. If you want to pair program on something with someone, you can just send them that ngrok URL, and they’ll be connected, live, to the server on your computer.
Hot reload works here in the exact same way that it does on your local computer. This is really powerful–you edit the source file on your computer, and it immediately hot reloads in your browser and in the browser of whoever you’re pairing with–now you tweak a shade of blue or an animation, and they can play with it on their end, in their own way (maybe they are screen-sharing their screen so you can see what they see). The traditional approach of sharing your screen is much inferior to this, I think; it doesn’t have that ability of independent interaction; it drains the other person of agency. They can’t look around and notice other things that are broken.
I also think that the over-the-Internet hot reload creates this feeling of connection, that you’re both touching this live wire, and there’s something really compelling about that. Not only does screen sharing dilute or prevent interaction, it doesn’t seem to create this same feeling.
There are also the advantages of code consistency and data consistency: if we’re pairing about some problem, and we’re both using my development server, and we refresh the page, we can be pretty sure that 1. we’re looking at the exact same map data and 2. we’re using the exact same version of the application. We’ll talk later about import and export as another route to data consistency.
I talked about the desire for immediate feedback earlier. Immediate feedback doesn’t just mean that the app reloads really fast. Immediate feedback means you should be able to see the thing that you care about immediately. The thing that you care about is not necessarily just ‘the app interface’ or ‘whether it works’; in fact, while you’re programming, you often want to see some particular data on the inside of your program, or you want to see how some performance metric is changing, etc.
Frontend programmers probably don’t do enough of this yet. React developer tools provide some of this observability, but it’s too clumsy, in my opinion (you’re clicking around this giant React component tree and squinting at blobs of JSON with latitudes and longitudes in them, instead of seeing the graphical Felt elements and the places on the map).
One concrete thing that we did toward this Felt-inspector dream: we exposed some variables, felt.$ and felt.$0, in the browser console.
You might know about $ and $0 in the browser console: these are variables that give you programmatic access to the DOM element that you've selected in the Web inspector.
It's been really useful to debug changes we make to the drawing system in Felt. These are the kinds of tools that we need–tools that provide us the ability to inspect an element, see its raw data (and if that matches up with what our frontend is showing), perturb it in various ways.
It also hints at the power of the browser console as a tool for developing applications, which I want to talk a little more about–
5. Browser Console
Your CSS styles all live in classes with obfuscated names (generated by some CSS-in-JS system), the CSS inspector isn’t designed to deal with that / even if you can tweak them in the inspector, you’ve lost most of the structure that you wrote in the original code
Transpiled variable names are mangled and you can’t even read them unless you also turn off source maps
Stack traces don’t work properly over async-await, or across a chain of React calls, or whatever, so you’re using console.log and breakpoints to reverse-engineer control flow
Anyway, the browser console is an excellent way to get more visibility into what the application is doing, and to do lightweight programming and scripting and analysis (without needing to edit the app itself).
It’s a nice JS environment which already has a lot of the context (all the normal libraries that we use in Felt can be imported, there’s a map open in the browser that you can take context from and draw onto, etc). Much easier than trying something in node at the command line, and much easier than actually developing inside the actual app and having to wire it up with the UI and with React and everything.
Also – as you can see above, the browser console supports CSS styles! It isn’t only the plain text that you might expect.
Something you might not know is that you can actually draw arbitrary images in the browser console. You can render into an HTML5 canvas, convert that canvas to a data URI, and then set that data URI as a CSS <p-inline>background-image<p-inline> in <p-inline>console.log<p-inline>. This is really useful for us (and, really, for any application where there is a visual-spatial element and you want to actually see what you’re developing).
Sometimes I’ve developed things purely as imperative browser commands first, and I get the logic right in that self-contained context (where I can scroll up and compare to past results!), and then I add it to where it belongs in the Felt app.
For example, I pan and zoom the map to the place where I want to test something, maybe draw some stuff in the app, then open up the console and iterate on some command that uses that location and/or the elements on the map.
The browser console also provides a nice interface to expose debug functionality without you having to make a proper HTML UI. You can make global functions to turn on debugging modes, to reveal different pieces of data, and so on, which leads us to–
6. Import and Export Data
Felt has a pretty complicated document model! Maps can be in all kinds of different states with all kinds of different objects with different properties/scale/location/zoom.
If I run into a problem, or I have some half-developed feature, and I want you to check it out, I want to be able to send you a copy of the map that I’m talking about, so you can look at it on your computer and try different things.
One solution is what I discussed earlier: be on the same server with the same backend database (which means either the production server, a development server deployed off a GitHub branch, or someone’s local development server over ngrok).
When that’s impractical, though, it’s also very useful to be able to quickly import and export objects; you can paste them into Slack or a GitHub issue comment or whatever you use (it’s also useful for debugging to be able to see what the ground-truth data looks like).
You want to have immediate feedback into what your changes are doing to the software – it’s great for motivation, it lets you play, try new things, and come up with solutions on the fly that you wouldn’t otherwise think of.
7. Build the Frontend the Backend in Parallel
Build all your views and interactions and transitions for real, but have them backed by state that lives purely on the client at first (in React local state, or in whatever state manager that you use – we use jotai).Then, later, when you have your backend working, switch to using your backend as the canonical state instead.
Sometimes it really is that simple! It can be, like, a five-line change. (Sometimes it takes a bit more work to migrate to using server-side state: we’ve had cases where the server wants to asynchronously change the data over time for security/resource use/postprocessing reasons, for example. We ran into this with image uploading: the client should render the uploaded image immediately, so it feels fast, but the server may replace it later as it postprocesses the image.)
There is some risk to this – that your frontend won’t quite be aligned with your backend – but it has worked well for us. I think you need the right framework and frontend data model to allow you to cleanly swap between frontend and backend state. I think that there is a huge psychological advantage to working this way, where you never feel blocked on anything on the frontend, and it doesn’t feel like a chore as you’re working on the interface because you have to change a bunch of points in the backend at the same time.
8. Start with the Prototype
This applies mostly if you have a design prototype from something like Figma or Sketch (or even a hand-drawn prototype). When you start developing that view for real, take that prototype, turn it into an image (a PNG file, etc.), and put that image right in the app (probably as a CSS <p-inline>background-image<p-inline> for the view that you’re working on). (You might set the CSS <p-inline>opacity<p-inline> for your real view to <p-inline>0.5 or something<p-inline>, so you can see through it to the prototype underneath and check how well they align.)
Then your development process is a process of continuously ‘fixing’ your real view, tuning it to match the prototype underneath, rater than trying to make it from scratch. You always have something working the whole time. You’re taking more and more of that dead static mock and tracing bits out and bringing them to life. A problem with this is that you need to have a good handle on your abstractions and primitives to do it well. It’s easy to just spew out hard-coded pixel values that make it match, but then you’re obscuring the structure of the design and making it brittle and hard to understand and change. You also don’t know how it’ll respond to resizes and theme changes and animation and so on.
Constant and Fast Iteration at Felt
At Felt, we believe the best products are built with constant and fast iteration. We go the extra mile at every point, whether it is cartography, frontend, infrastructure, to lower the distance between having an idea and seeing it live on screen. As a company that builds mapping products on the web, the frontend iteration cycles are especially important. In this post, I tried to list some of the less obvious ways to squeeze out some feedback loops. This work is exciting, but we are nowhere near done! We are always looking for amazing people to join.
I think that there is a huge psychological advantage to working this way, where you never feel blocked on anything on the frontend, and it doesn’t feel like a chore as you’re working on the interface because you have to change a bunch of points in the backend at the same time.