Write less code
The most important metric you’re not paying attention to
All code is buggy. It stands to reason, therefore, that the more code you have to write the buggier your apps will be.
Writing more code also takes more time, leaving less time for other things like optimisation, nice-to-have features, or being outdoors instead of hunched over a laptop.
In fact it’s widely acknowledged that project development time and bug count grow quadratically, not linearly, with the size of a codebase. That tracks with our intuitions: a ten-line pull request will get a level of scrutiny rarely applied to a 100-line one. And once a given module becomes too big to fit on a single screen, the cognitive effort required to understand it increases significantly. We compensate by refactoring and adding comments — activities that almost always result in more code. It’s a vicious cycle.
Yet while we obsess — rightly! — over performance numbers, bundle size and anything else we can measure, we rarely pay attention to the amount of code we’re writing.
Readability is important
I’m certainly not claiming that we should use clever tricks to scrunch our code into the most compact form possible at the expense of readability. Nor am I claiming that reducing lines of code is necessarily a worthwhile goal, since it encourages turning readable code like this...
for (let let i: number
i = 0; let i: number
i <= 100; let i: number
i += 1) {
if (let i: number
i % 2 === 0) {
var console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and
process.stderr
. The global console
can be used without calling require('console')
.
Warning: The global console object’s methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
log(`${let i: number
i} is even`);
}
}
...into something much harder to parse:
for (let let i: number
i = 0; let i: number
i <= 100; let i: number
i += 1) if (let i: number
i % 2 === 0) var console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and
process.stderr
. The global console
can be used without calling require('console')
.
Warning: The global console object’s methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
log(`${let i: number
i} is even`);
Instead, I’m claiming that we should favour languages and patterns that allow us to naturally write less code.
Yes, I’m talking about Svelte
Reducing the amount of code you have to write is an explicit goal of Svelte. To illustrate, let’s look at a very simple component implemented in React, Vue and Svelte. First, the Svelte version:
How would we build this in React? It would probably look something like this:
import import React
React, { import useState
useState } from 'react';
export default () => {
const [const a: any
a, const setA: any
setA] = import useState
useState(1);
const [const b: any
b, const setB: any
setB] = import useState
useState(2);
function function (local function) handleChangeA(event: any): void
handleChangeA(event: any
event) {
const setA: any
setA(+event: any
event.target.value);
}
function function (local function) handleChangeB(event: any): void
handleChangeB(event: any
event) {
const setB: any
setB(+event: any
event.target.value);
}
return (
<type div = /*unresolved*/ any
div>
<type input = /*unresolved*/ any
input type="number" value={a: any
a} onChange={handleChangeA: (event: any) => void
handleChangeA} />
<type input = /*unresolved*/ any
input type="number" value={b: any
b} onChange={handleChangeB: (event: any) => void
handleChangeB} />
<type p = /*unresolved*/ any
p>
{a: any
a} + {b: any
b} = {const a: any
a + const b: any
b}
</p>
</div>
);
};
Here’s an equivalent component in Vue:
<template>
<div>
<input type="number" v-model.number="a">
<input type="number" v-model.number="b">
<p>{{a}} + {{b}} = {{a + b}}</p>
</div>
</template>
<script>
export default {
data: function() {
return {
a: 1,
b: 2
};
}
};
</script>
In other words, it takes 442 characters in React, and 263 characters in Vue, to achieve something that takes 145 characters in Svelte. The React version is literally three times larger!
It’s unusual for the difference to be quite so obvious — in my experience, a React component is typically around 40% larger than its Svelte equivalent. Let’s look at the features of Svelte’s design that enable you to express ideas more concisely:
Top-level elements
In Svelte, a component can have as many top-level elements as you like. In React and Vue, a component must have a single top-level element — in React’s case, trying to return two top-level elements from a component function would result in syntactically invalid code. (You can use a fragment — <>
— instead of a <div>
, but it’s the same basic idea, and still results in an extra level of indentation).
In Vue, your markup must be wrapped in a <template>
element, which I’d argue is redundant.
Bindings
In React, we have to respond to input events ourselves:
function function handleChangeA(event: any): void
handleChangeA(event: any
event) {
setA(+event: any
event.target.value);
}
This isn’t just boring plumbing that takes up extra space on the screen, it’s also extra surface area for bugs. Conceptually, the value of the input is bound to the value of a
and vice versa, but that relationship isn’t cleanly expressed — instead we have two tightly-coupled but physically separate chunks of code (the event handler and the value={a}
prop). Not only that, but we have to remember to coerce the string value with the +
operator, otherwise 2 + 2
will equal 22
instead of 4
.
Like Svelte, Vue does have a way of expressing the binding — the v-model
attribute, though again we have to be careful to use v-model.number
even though it’s a numeric input.
State
In Svelte, you update local component state with an assignment operator:
let let count: number
count = 0;
function function increment(): void
increment() {
let count: number
count += 1;
}
In React, we use the useState
hook:
const [const count: any
count, const setCount: any
setCount] = useState(0);
function function increment(): void
increment() {
const setCount: any
setCount(const count: any
count + 1);
}
This is much noisier — it expresses the exact same concept but with over 60% more characters. As you’re reading the code, you have to do that much more work to understand the author’s intent.
In Vue, meanwhile, we have a default export with a data
function that returns an object literal with properties corresponding to our local state. Things like helper functions and child components can’t simply be imported and used in the template, but must instead be ‘registered’ by attaching them to the correct part of the default export.
Death to boilerplate
These are just some of the ways that Svelte helps you build user interfaces with a minimum of fuss. There are plenty of others — for example, reactive declarations ($:
statements) essentially do the work of React’s useMemo
, useCallback
and useEffect
without the boilerplate (or indeed the garbage collection overhead of creating inline functions and arrays on each state change).
How? By choosing a different set of constraints. Because Svelte is a compiler, we’re not bound to the peculiarities of JavaScript: we can design a component authoring experience, rather than having to fit it around the semantics of the language. Paradoxically, this results in more idiomatic code — for example using variables naturally rather than via proxies or hooks — while delivering significantly more performant apps.