3881 stories
·
3 followers

Mechanical Watch

2 Shares

In the world of modern portable devices, it may be hard to believe that merely a few decades ago the most convenient way to keep track of time was a mechanical watch. Unlike their quartz and smart siblings, mechanical watches can run without using any batteries or other electronic components.

Over the course of this article I’ll explain the workings of the mechanism seen in the demonstration below. You can drag the device around to change your viewing angle, and you can use the slider to peek at what’s going on inside:

What you see here is known as the movement – the inner part of a mechanical watch that’s usually enclosed in a metal case. In this article I’m focusing on a watch movement itself, since beautiful watch cases merely hide the intricate mechanisms which are the real stars of the show.

The entire watch movement has a lot of parts, and in this blog post I’ll explain the purpose of each one. The world of watchmaking is jargon-heavy, so many of the components may have unfamiliar names, but you shouldn’t feel pressured to remember them – the names and parts will be color-coded for easy reference.

In a functioning watch many parts are in constant motion. By default all animations in this article are enabled, but if you find them distracting, or if you want to save power, you can globally pause all the following demonstrations.disabled, but if you prefer to have things moving as you read you can globally unpause them and have animations running.

While the entire watch movement has many parts, the timekeeping system, which forms the core function of any watch, consists of just seven major elements which we can lay out in a straight line:

It may not look like much, but these parts still have a lot of interesting details about them that contribute to the second hand rotating at a correct pace. We’ll start exploring these details by focusing on the source of power for this entire contraption.

Power

Purely mechanical devices have a few different ways to power themselves, but one of the simplest methods to store energy is to use a spring. Most springs we see in daily life are coil springs. In the demonstration below, you can move the mass attached to this type of spring to see it bounce:

When a spring like this is compressed, it stores some energy that is then released when the compressing tension is removed. Mechanical watches typically use a different kind of spring – a spiral torsion spring. This type of spring is loaded when it’s twisted. When let go, the spring unwinds in the opposite direction to eventually settle in its natural state:

In a mechanical watch, we ultimately want to show rotating hands, so a spinning motion that a torsion spring provides is particularly useful. A spring in a typical mechanical watch has a slightly more complicated shape – you can see it below in its relaxed state. By dragging the slider you can try to wind it midair, but as soon as you let go, it will snap back to its original shape:

As you can see, this spring is quite strong and it wants to expand very rapidly. To contain the spring we have to put it in a casing known as a barrel:

Once in the barrel, the spring still wants to expand to its original state, but the barrel’s wall keep it in place. This spring is the storage of energy for the watch and its name, the mainspring, reflects its importance.

Unfortunately, we can’t really get any useful work from the mainspring in this state – it has already expanded to the largest possible size. To store more energy in it we need to wind it tightly using the arbor that we’ll first attach on the inner side of the mainspring:

If you look closely, the mainspring has a little hole near its end – you can see it in the center of the demonstration. The arbor has a little hook that grabs onto that hole:

When the arbor is turned, it pulls the mainspring with it, causing it to wind. In the demonstration below, we’re holding the barrel tight, and you can turn the arbor by dragging the slider:

Notice that as soon as you let go of the arbor by releasing the slider, the mainspring will turn the arbor right back. This is less than desired – we want the barrel to turn instead, so that it can power the other parts of the watch. To get some useful work from the mainspring, we’ll have to keep holding on to the arbor and instead let the barrel go when we want to use the stored energy:

We’ll soon see how this is accomplished in practice, but for now we’ll assume that the arbor is held tight and the mainspring ends up rotating the barrel, just like in the demonstration above. Before we finish up with the mainspring and the barrel, let’s discuss two other details that make this mechanism more reliable. Let me bring up the relaxed spring one more time:

The metal strip attached to the mainspring provides additional tension to its outer part. That metal strip really wants to snap back to its straight shape, so it pushes against the wall of the barrel, creating a lot of friction that keeps the mainspring in place:

This locks the outer end of the mainspring when the arbor moves the inner. If we were to keep winding the spring past its maximum capacity, we’d overpower that friction letting the mainspring slip inside – this acts as a safety mechanism to prevent the parts from breaking.

As we’ve seen, in its relaxed state, the mainspring forms an S-shape with varied curvature throughout. This helps to balance the tension in mainspring’s different sections when it is inside the barrel. Notice that the inner sections of the wound spring have a much smaller radius than the outer parts. If the relaxed spring was just a straight piece of metal, then after winding, the inner parts would be bent much more than the outer parts. With the S-shaped spring the outer sections of the spring are also under a similar tension because they want to get back to their curve that is bent in the opposite direction.

To secure the mainspring and prevent dust from getting in we close the barrel with a lid that snaps into its place:

We’ve managed to make some parts rotate and one could naively think that we could just attach a watch hand to the barrel to make it track time. Unfortunately, that won’t really work – you can witness this in the demonstration below. You can see how this “watch” behaves after you wind the mainspring with the slider and let it go:

We clearly have some work to do – the hand spins way too fast and it only does a few rotations before the mainspring inside the barrel runs out of the stored energy. Clearly, this contraption won’t let us track time in any reliable way.

If we wanted our watch to run continuously for around 40 hours on a single wind, we’d need the minute hand to complete 40 rotations in that time. Moreover, the second hand should cover around 40 × 60 = 2400 complete rotations in that time. We need to find a way to convert a small number of revolutions of the barrel into a large number of revolutions of the hands. This is where gears come in.

Gears

I’ve talked about gears on this blog before, so let me just recap things very briefly. Gears can be used to change the speed of rotation between two different axes. In the demonstration below, you can witness that by observing little dots I put on each gear – the yellow gear, which is powered by the bigger red gear, takes much less time to finish a single revolution:

An important aspect of two matching gears is their number of teeth. Each tooth in one gear meets with a space between teeth in the other gear, so within a unit of time both gears rotate by the same number of teeth. If the number of teeth in two gears is different, those gears can take a different amount of time to complete a single rotation. In the demonstration below, you can change the ratio of the number of teeth between the driving red gear and the driven yellow gear to see how it affects the speed of rotation of that yellow gear:

These gears are intended to work with each other so the ratio of teeth is equivalent to the ratio of the gear radii. When the driving gear has more teeth than the driven gear, the driven gear makes more rotations than the driving gear. We can use this behavior to make the second hand of a watch rotate many times on a single rotation of the barrel.

Let’s consider how much of a speed increase we have to do here. The barrel can rotate close to 7 times on a single wind, but we want the second hand to complete around 2400 revolutions in the same time. We need the ratio of teeth, or the ratio of radii, to be around 343:1. Let’s see how that would look in practice. In the demonstration below, you can use the slider to look at the two gears from further away:

As you can see, these proportions are ridiculous – to make the red gear fit in any reasonably sized watch, the yellow gear would have to be absolutely tiny and both gears would have to have very fragile, microscopic teeth.

Instead, mechanical watches use a train of gears with multiple gears working in pairs – each pair increases the speed to some extent. In the demonstration below, you can see the four wheels participating in this reduction. Notice that there are two gears on most axes of rotation. You can control the speed of rotation of this gear train using the slider:

The barrel acts as the first wheel, it drives the second wheel, which drives the third wheel, which finally drives the fourth wheel. Notice that each big gear drives a smaller gear called a pinion. A pinion is mounted on the same shaft as the next big gear so we’re able to keep increasing the speed on each axis. This approach has significant advantages – we’re able to make the overall mechanism much smaller and we’ll soon use one of the intermediate wheels that rotates at a slower rate to drive minute and hour hands.

Before we finish up with gears, let me quickly mention the shape of their teeth. While many bigger machines use an involute shape for the profile of their gear teeth, mechanical watches commonly use cycloidal profiles which are obtained by rolling a circle on the surface of another circle.

Let’s see how the so-called going train that we’ve assembled works when we wind the mainspring through the arbor and let the watch run:

We’ve certainly achieved the goal of the second hand rotating many times on a single rotation of the barrel, but the speed of revolution of that hand is still completely untamed. We need to find a way to control the rate of release of the energy stored in the mainspring – we’ll do this with the escapement.

Escapement

Let’s start by looking at the two components that create the escapement – the escape wheel and the pallet fork:

Notice the unusual shape of the teeth of the escape wheel – it’s very different than the gears we’ve seen before. Its top part hosts a regularly shaped gear that can be used to turn that wheel.

The pallet fork itself is made of metal, but notice the two pinkish transparent parts at its end. These are jewels made from synthetic ruby. That compound is not only very hard, which prevents its wear, but it also has a low coefficient of friction with steel. Let’s see why these properties are important by observing how these two components interact with each other:

The escape wheel wants to rotate as indicated by the red arrow. The pallet fork prevents that motion, but as we pivot that pallet fork back and forth we let the escape wheel briefly escape from that jail only to be stopped again.

We’ll see the details of that interaction in a few paragraphs, but right now this mechanism lets us control the rotation of the escape wheel by simply moving the pallet fork from one side to another. Let’s see how these pieces fit into the rest of the assembly. In the demonstration below, I’ve wound the spring for you so the barrel, through the gear train, ends up trying to rotate the escape wheel. Using the two buttons you can switch the position of the pallet fork:

The mainspring wants to unwind by rotating the escape wheel, but the pallet fork only allows this to happen for a brief period of time. Because of the gear reduction, the rotation of the barrel is pretty much invisible. However, if you observe the hand attached to the fourth wheel, you can see it gently rotate as you swing the pallet fork back and forth.

The little time keeping mechanism is almost fully functional now. The last remaining piece here is a device that will automatically tick the pallet fork back and forth. However, for the watch to track time correctly that ticking action has to happen at an appropriate cadence. This is where the balance comes in – it forms the beating heart of a watch.

Balance

Let’s bring up the first torsion spring we saw before – recall that once you twist it from its original position, it will oscillate back and forth, only to settle after a while:

We can control the rate of this periodic motion by adjusting two parameters. The first one is the stiffness of the spring, which primarily depends on its height, thickness, and length, as well as the type of material from which it’s made. The second one is the mass and its distribution, or, more precisely, the moment of inertia of the object that the spring rotates. Moment of inertia increases when more mass is put further away from the axis of rotation. In the demonstration below, you can tweak both the stiffness of the spring and moment of inertia of the attached mass to see how these parameters affect the period of rotation:

By carefully tuning these parameters, we can make this system oscillate at a desired rate. This idea of using a torsion spring with attached mass is exactly what mechanical watches use as their source of precise time tracking. The balance is formed by the balance wheel attached to the balance spring. In this watch the balance wheel oscillates back and forth at a fairly high frequency:

At the bottom side of the balance wheel you’ll find another pinkish transparent jewel called jewel roller. While small, this part is very important – this jewel hits the other end of the pallet fork as the balance wheel rotates, which in turn pushes the pallet fork back and forth. Let’s first look at an overview of how the balance wheel interacts with the other parts. In the demonstration below, you can slow things down with the slider:

Let’s look at this interaction up close, as it deserves a closer attention. In the demonstration below, you can scrub back and forth in time to see all the action as it happens:

balance wheel continues its swing

As the balance wheel swings, the jewel roller strikes the pallet fork, which unlocks the escape wheel. Once unlocked, the escape wheel powered by the mainspring pushes on the pallet fork which, through the jewel roller, pushes on the balance wheel itself. This causes the balance wheel to gain some energy, which prevents it from stopping after a while – it’s equivalent to giving a push to a person swinging on a swing. When the balance wheel comes back, it performs the same action, just in the other direction.

You may also have noticed a subtle dance between the little horn at the end of the pallet fork and the notched disk on the balance wheel. Those parts make sure that the pallet fork can switch sides only at the appropriate time – it’s a safety mechanism that prevents the watch from locking up when the watch is shaken or dropped:

Once the pallet fork unlocks the escape wheel, that wheel has to start spinning very quickly. This is why gears in the gear train have holes in them – it reduces their moment of inertia so that the barrel can accelerate them more quickly.

It’s also important to mention that the gear train not only increases the speed of the gears, but it also reduces the forces acting on the balance. The barrel itself turns quite forcefully but at the escape wheel the torque is greatly reduced, which prevents the escape wheel from pushing the pallet fork and thus the balance wheel with too much vigor.

Let’s look at the entirety of what we’ve built so far one last time. I’m now running the mechanism at its normal speed:

In this watch movement the balance wheel does a full back and forth swing four times per second, hitting the pallet fork twice during each cycle, for a total of 8 beats per second or 28,800 beats per hour. While different watches may have different rates, they all do a tiny turn of the second hand many times per second, which gives mechanical watches the illusion of a very smooth hand motion.

In principle, all the pieces we have here are sufficient for the watch to run, but we’re still missing a few details. More importantly, we’ve just been hanging the parts in the air, so it’s time we started a proper assembly of the complete watch movement.

Mainplate

We’ll start with the mainplate, which forms the main body of the movement:

Notice that it has a lot of different openings – we’ll fill them in by the end of this article. The pink elements are yet again ruby jewels. They form bearings in which the axes of various components can rotate. Let’s look at a simple jewel up close:

Notice that a jewel has a small basin in it. To even further reduce energy losses of the rotating components, a small amount of special oil is placed in that cavity. That oil sticks to the jewel and a shaft that rotates inside it to further decrease the friction, which lets the watch run longer on a single wind, while also reducing wear on the delicate mechanical parts.

The first two components we will mount onto the mainplate are the escape wheel and the pallet fork:

The pallet fork itself is then topped with the pallet fork bridge. That bridge holds the other end of the pallet fork’s axis, and it is attached to the mainplate with two screws:

Notice that in this watch the side-to-side movement of the pallet fork is limited by the shape of the two knobs in the central part of the pallet fork bridge:

This ensures that the escape wheel can only push the pallet fork so far before the motion is physically stopped by these knobs.

Next, we can put the rest of the gear train in. All four wheels are cleverly arranged so that they occupy only a small amount of space:

Notice that the fourth wheel goes directly through the center of the watch – you can see its axis poking on the other side. By the end of our assembly we’ll attach a second hand on the end of that long axis. To secure all elements in place, we cap them with a train wheel bridge, which provides the setting for the other ends of the shafts for all rotating parts. That bridge is screwed to the mainplate to hold everything in place:

The only remaining part from the initial mechanism that we haven’t yet mounted is the balance, which forms its own little assembly. Let’s build it up first by attaching all the parts to the balance bridge:

Notice that the balance spring is very delicate and the balance wheel ends up stretching it out. Because of its thinness, the balance spring is often referred to as hairspring. The yellow and teal components both regulate the behavior of the balance. Let’s see how they work in action:

The yellow components are firmly attached to the balance spring, and by turning them, we can adjust the resting position of the balance wheel and its jewel roller. This ensures that both the “tick” and “tock” phases of the balance wheel swing take the same amount of time.

The teal components can freely slide on the hairspring, but they reduce or increase its effective length as they prevent the tail section of the hairspring from oscillating freely. By adjusting position of these teal components we can modify the duration of a single beat and make the watch run slightly faster or slower. That speed regulation can also be fine-tuned using the screw in the top part – its head is not centered, so when turned it will gently rotate the little teal fork.

The hairspring is made from special alloys like Nivarox that keep the spring’s stiffness invariant to temperature differences, which improves the overall timekeeping accuracy.

The final portion of the balance assembly is the shock protector mechanism, which consists of the casing, two jewels, and a tiny spring that keeps everything in place:

This mechanism protects the fragile tips of the balance shaft from breaking when the watch experiences a sudden jerk. Let’s see how these pieces act together when the balance shaft is jolted around:

When the watch is shaken, the motion of the shaft is absorbed by the spring, similarly to the suspension system in a car. If the jerk is very strong, then the much thicker and stronger part of balance shaft carries the load through the case, which protects the fragile tip from breaking.

Let’s attach the entire balance assembly to the rest of the movement we’ve built so far. Notice that the other end of the balance wheel’s axis also rests on the shock protection jewels embedded in the mainplate:

With that last step, we’ve actually finished recreating the core of the watch mechanism that we’ve previously seen floating in the air. However, you may remember that I’ve glanced over the little detail of how to make sure that the mainspring stays wound. Let’s see what happens if we actually try to wind the watch using the arbor. For the sake of clarity I also cut a hole in the top part of the barrel so that you can see the spring inside:

As long as the arbor is held, the mainspring can power the rest of the watch – you can see the rotation of the second hand attached to the fourth wheel on the other side of the watch. However, as we let the arbor go the mainspring finds an easy way to release its tension by just turning the arbor back – the spring quickly losses all its stored energy and the watch stops.

To prevent the mainspring from unwinding on its own, we need to restrain the arbor from turning counterclockwise, while still allowing the clockwise rotation so that we can wind the spring. This seemingly complicated problem is solved with a very simple mechanism known as the click – let’s see how it works.

Click

To continue developing our assembly, we firstly need to put a solid foundation in the form of the barrel bridge – it holds the barrel in place and provides structure for other parts. Since this bridge will make some areas inaccessible, we’re also going to attach a little lever that we will get back to at a later point:

Then, we’ll screw in the ratchet wheel onto the arbor. Notice that the ratchet wheel has a square opening, which matches the square shape of the top part of the arbor:

Those matching square shapes will cause the arbor to turn when the ratchet wheel is turned. I temporarily removed the screw to make things easier to see:

Here come the three critical pieces of the puzzle. Firstly, we put the little click in the opening on top of the barrel bridge:

Within its limited range the click can rotate back and forth on its little axis:

The second piece of the puzzle is a click spring. This little piece of metal is very springy. When we squeeze it, it wants to pop back:

We compress that click spring a little and we also put it into the barrel bridge:

Notice that when we try to rotate the click, the click spring will push it back in place as soon as we let go:

The final piece of the puzzle is the crown wheel, which also lands on the barrel bridge. It’s secured in a place with a screw with a left-handed thread – unlike most regular screws this one is fastened when turned in the counterclockwise direction:

Notice how the teeth of the crown wheel interact with the ratchet wheel. While it looks as if the crown wheel was missing every other tooth, the two gears can still mesh and function together. The gaps in the crown wheel allow the little post on the click to fall between the crown wheel’s teeth.

If we turn the crown wheel counterclockwise, it will mesh with the ratchet wheel and wind the spring. Notice how the teeth of the crown wheel end up pushing the click away, but it snaps back as soon as there is some space:

When the click snaps back and hits the crown wheel, it makes a clicking sound, which explains its name.

The counterclockwise turn of the crown wheel allows us to wind the mainspring, so let’s see what happens when we try to turn it in the opposite direction. In the simulation below, notice how the crown wheel’s teeth jam with the click, preventing the crown wheel’s rotation:

This simple mechanism allows us to wind the mainspring by turning the crown wheel, which you can do in the demonstration below. The click also prevents the mainspring from unwinding on its own – that’s why you can’t drag back the slider without restarting the entire simulation:

The second hand on the other side of the watch shows how the seconds are tracked, but a functional watch should show minutes and hours as well. Let’s see how this watch movement accomplishes these goals with a set of gears that form the so-called motion works.

Motion Works

In our movement, the second hand is cleverly mounted on the fourth wheel of the power train since that wheel rotates once per minute with high precision. For the minute hand to turn at the correct pace, we need some axis to rotate 60 times slower than that. Thankfully, the designers of this watch movement used an ingenious way to harness some of that speed reduction from the other gears.

If you look closely, you can see that the small gear of the third wheel from the other side of the watch is exposed through a little opening. We can mount a cannon pinion with its driving wheel onto the center of the watch and have that driving wheel mesh with the small gear:

When that third wheel rotates, it turns the driving wheel and thus the cannon pinion. By mounting the minute hand on that cannon pinion we can keep track of passing minutes – the number of teeth in all the involved gears is carefully calculated to achieve the desired 60 times speed reduction compared to the second hand.

Let’s see the functional second hand and minute hand in the demonstration below. The slider lets you control the speed of flowing time so that you don’t have to wait too patiently to see hands change their position:

The hour hand itself needs to rotate 12 times slower than the minute hand, but we can easily achieve that using two additional gears. The intermediate minute wheel meshes with the cannon pinion, and the hour wheel meshes with the pinion of that minute wheel:

The hour wheel can loosely rotate on the cannon pinion so that they can both turn independently of each other. By putting the hour hand on that hour wheel, we can finish assembling the mechanism that drives the hands of the watch. I’ve also attached a dial that has each of the twelve hours marked – it actually lets us read the time that the hands are showing:

Time keeping is the fundamental function of every watch, but many devices go beyond that by adding various additional features known as complications. While our movement is not very sophisticated, it still has a nice complication that shows the current day of the month right in the little window on the right side of the dial. Let’s see how this feature is implemented.

Date

The date mechanism in this watch consists of four major parts – the jumper spring, the indicator gear, the date jumper plate with its gear, and the big date ring itself with all possible 31 days imprinted on it:

To explain how this mechanism works I’ll first hide all the unrelated parts. I’ll also remove the cover from the indicator gear, which reveals a small torsion spring hidden inside it. Let’s see how these pieces work together when the hour wheel rotates. You can go back and forth in time using the slider:

As the hour wheel turns, it rotates the gear in the date jumper plate. The other side of that gear then turns the indicator gear and the torsion spring attached to it. That spring snags onto a tooth on the date ring and gets flexed, but at some point it starts to push the date ring forward. When the ring rotates enough the jumper spring rapidly snaps the ring to the next position.

You may wonder why we need this complicated mechanism in the first place. One could naively assume that we could directly tie the rotation of the date ring to the rotation of the hour wheel, similarly to how we rotated the hour wheel in sync with minutes, albeit at slower pace. Unfortunately, this would cause the current date to continuously rotate under the little window in the dial, making it hard to read. You can see that behavior on the left side in the demonstration below:

On the right side you can see the date indicator as operated by the mechanism that we’ve just built – the date only changes around midnight. You may have realized that the date tracking in our movement is not particularly smart. This watch always counts 31 days every month, so we have to change the date a day after a shorter month occurs. Moreover, if the watch hasn’t been running for a while, the time itself may be incorrectly set. We need to find a way to adjust date and time on our watch.

Thankfully, gears driving the minute hand, the hour hand, and the date indicator are all connected, so we can adjust everything by turning a single gear. I’ll briefly hide the hour wheel to make things visible:

Notice that when we turn the minute wheel only the cannon pinion turns. That pinion fits tightly inside its driving gear – it usually turns with that gear. However, when the driving gear can’t rotate because it’s blocked by the rest of the gear train, the cannon pinion can overpower the friction of that tight fit and rotate on its own. This lets us set time without interfering with the gear train, which could break the delicate parts.

With the hour wheel in place, rotation of the minute wheel also sets the hour, and, if we turn that gear long enough, the date:

With every step our watch is becoming more complete, but we still have a few inconveniences in our way. To change the time and to wind the mainspring, we have to turn the internal gears of the movement, which normally are safely hidden inside the watch case.

Moreover, on every month that lasts less than 31 days, we currently have to tweak the time setting, as that’s the only way to adjust the date. Ideally, we’d find a way to set the date separately from the time.

To fix these problems we’ll assemble the keyless works which is a mechanism that will let us resolve all these issues.

Keyless Works

Firstly, let’s look at the crown, which is the main interface for operating the watch, and the stem that is attached to that crown:

The crown sits freely on the outside of the watch and is directly touched by the user. Notice that part of the stem has a square cross section. The stem carries two additional components – the winding pinion and the sliding pinion. First, let’s slide them on to see how they fit:

The winding pinion has a circular opening so it can rotate on the stem easily. However, the sliding pinion has a square opening which aligns with the section of the stem that has a square shape. That square interlocking causes the sliding pinion to rotate with the stem as the crown turns:

Let’s put these pieces into the main assembly. I temporarily removed the date ring so that it doesn’t get in our way:

Notice that the winding pinion meshes with the crown wheel on the other side of the watch. To actually turn the winding pinion we first have to move the sliding pinion all the way towards it – I symbolize this pushing force with the blue arrow below. If we now turn the crown the matching shape of the neighboring surfaces on the winding pinion and the sliding pinion causes them to interlock. We’re ultimately able to turn the crown wheel and the rest of the mainspring-winding machinery by turning the crown clockwise:

However, if we rotate the crown in the other direction, the shape of the neighboring surfaces will push the sliding pinion away, because the crown wheel, and therfore the winding pinion, can’t rotate in the opposite direction. This safety mechanism ensures that any forceful rotation of the crown in the “wrong” direction won’t break the movement.

It seems that we’ve achieved our goal of being able to wind the spring by simply turning the crown. Unfortunately, we still have a small problem to solve – we need something to actually exert the force that pushes the sliding pinion towards the winding pinion.

Moreover, in some cases we want the rotation of the crown to serve different purposes. Other than winding the mainspring, in our watch we want to be able to adjust the date, and, separately, the time. We’ll choose each of those three actions by pulling the crown in and out.

Let’s build a mechanism that will solve these problems. Firstly, we’ll put the corrector lever and the setting lever in place:

If we now pull the crown in and out, these parts will rotate on their little pivots with a fairly complex interaction between them:

With the other parts in the way it may be hard to see what’s going on, so let’s look at these components on their own. Notice the intricate interlocking that happens when we pull the crown in and out with the slider:

A groove in the stem ends up locking with a small post in the setting lever, causing it to rotate as the crown is pulled. The other post on the setting lever ends up pushing and hooking with the corrector lever, making it rotate as well.

So far the mechanism doesn’t do anything interesting, so let’s put the setting wheel on top of the corrector lever:

That wheel can move freely on its post. If we now pull the crown in and out we can see that the setting wheel engages with the minute works:

By turning that setting wheel we’ll be able to set time on the watch, but to turn that wheel we need to slide the sliding pinion towards it so that the rotation of the crown and the attached sliding pinion rotates the setting wheel:

This poses a challenge – we need to control the position of the sliding pinion to, depending on the mode, engage the winding pinion to wind the mainspring, or the setting wheel to set the time. This is where the yoke comes in:

In the close-up down below you can observe that yoke fits into the groove on the sliding pinion, so as the yoke rotates on its pivot, it will push the sliding pinion in and out, causing it to slide. Additionally, the yoke itself is pushed by the setting lever as we pull the crown:

We’re almost done with this little mechanism, we just need to finish the little details. Firstly, we want to keep all the fragile pieces in place – right now nothing prevents them from falling off their careful placement. Secondly, when we pull the crown, there are no distinctive stops in its movement – by turning the crown we may accidentally change the current mode. Finally, when we push the crown all the way in to switch back to the winding mode, we want the yoke to reliably return to its initial position. This is where the setting lever jumper comes in – it serves all three of these purposes:

That part is screwed to the mainplate, which prevents the other parts from falling out. Its various arms and legs also help to keep the things pressed down. Let’s see how the setting lever jumper helps us with other two problems. Notice the three small grooves that I’m pointing out with the gray arrows:

As we pull the crown in and out, the small post in the setting lever ends up snapping into one of those three places. To jump between the grooves, that small post has to bend the long arm of the jumper, which creates tension that pushes that small post into the closest groove. We end up with three distinct positions that all the pieces can rest in – once locked we can reliably turn the crown without risk of accidentally changing the current mode.

Finally, on the other end of the setting lever jumper we also have a thin section that is kept under tension against the yoke – I’m pointing its location with a gray arrow:

As the yoke rotates, that springy piece of metal wants to rotate the yoke back. When the crown is in the date or time setting mode, the setting lever prevents the yoke from coming back, but once we return to the winding mode, that spring in the jumper will rotate the yoke back causing the sliding pinion to move back as well.

There is actually one additional clever bit that’s been hiding in plain sight. If you recall, we put a small lever right on the mainplate before we started working on the winding mechanism. The short end of that lever fits in the groove of the sliding pinion. When we pull the crown and move the sliding pinion, that lever rotates:

When turned all the way, that lever rubs against the balance wheel preventing it from moving – this stops the watch. As a result, when we pull the crown all the way out to enter the time setting mode, that stop lever blocks the balance wheel, which stops the watch in an action known as hacking. This lets us set the time without the second hand changing on its own at the same time, aiding with more precise time adjustment.

Let’s look at the functions of this entire mechanism one more time with all the participating pieces in place. When the crown is full pushed in, its rotation will rotate the sliding pinion, which turns the winding pinion, and then the crown wheel, and finally the ratchet wheel to wind the mainspring:

When the crown is pulled all the way out, its rotation turns the sliding pinion, the setting wheel, and then the minute wheel, the hour wheel, and the hidden cannon pinion which allows us to set the time:

Finally, when the crown is pushed roughly halfway through, we enter date setting mode, but to see it work we still need to attach an additional date corrector that fits into the small groove on the mainplate:

Notice that the date corrector can freely slide up and down in that groove. If we now pull the crown out mid way and turn it, we end up rotating that date corrector, which then can engage with the teeth on the inside of the date ring. The date jumper spring makes sure that we lock the date ring at a valid position:

Personally, I think this entire mechanism known as the keyless works is a real mechanical marvel. The intricate interactions are so well balanced and each part serves many different roles. Older pocket watches were wound by a separate key, with the crown being used only to set the time, but modern watches get away with not having a winding key, which explains the keyless name. With just a few carefully shaped pieces and a single crown, we can control various settings of the watch. Before we move on, let’s secure the remaining pieces with the minute train bridge:

We’re almost done building the watch movement. The final component that we’ll assemble will make the watch automatically wind itself as we roam around.

Automatic Winding

When the person wearing a watch moves arms throughout the day, the orientation of that watch in space changes quite a lot. Even during a leisurely walk, the watch swings slightly relative to the ground. Normally, all the energy used to move the watch goes to waste, but an automatic winding mechanism manages to capture some of it to wind the mainspring.

Let’s first try to understand the main idea by attaching the complete automatic winding mechanism to the watch. Its primary part is the weight that can rotate freely around the center. When that weight rotates it drives a bunch of gears, with the last one connecting to the ratchet wheel that is used to wind the mainspring hidden inside the barrel:

The fact that the weight can rotate freely is critical here. In the demonstration below, you can witness what happens to the weight as you rotate the watch in space by dragging it around. The gravity works towards the bottom of this website – it always pulls the weight down, which makes it turn relative to the rest of the watch:

If you recall our discussion of watch winding, you may remember that the ratchet wheel can only turn in one direction with the click preventing the mainspring from just unwinding on its own. However, the weight can swing back and forth, which would normally imply that any gear system that is connected to that weight would also rotate in both directions.

If you look at the automatic winding mechanism on its own, you can witness something unusual – as you turn the weight back and forth with the slider, the output gear turns only in one direction. I put a little black dot on that gear to make it easier to see:

To understand how this happens let’s first look at all the parts involved in the mechanism:

The green gear is attached directly to the bottom of the weight, so when the weight rotates, that gear turns the two blue gears on the underside of the yellow gears. Most of this composition is similar to things we’ve seen before with gears kept in place by bridges. However, you may have guessed that the doubled-up pairs of yellow and blue gears are responsible for the magic here. Let’s see how they’re constructed:

The blue gear can rotate freely on the yellow gear, and the fish-like levers can also rotate around their axis through the holes in the blue gear. Notice that the inner part of the yellow gear has a particular shape. In the demonstration below, I removed most of the central part of the blue gear so that you can see what’s going on inside. You can rotate that gear back and forth using the slider to see how the parts interact:

Notice that when you rotate the blue gear counterclockwise, the levers just slide through the internals of the yellow gear. However, when you rotate the blue gear clockwise, one of the levers gets stuck and it starts to turn the yellow gear with it. This clever mechanism transfers power from the blue gear to the yellow gear only in one direction.

The autowinding assembly contains two such gears – one will drive the output gear when turned clockwise, and one that turns that gear when turned counterclockwise. In the demonstration below, you can witness what happens when you rotate the gear attached to the weight. To make things easier to see I removed all of the non functional parts:

Notice that I’m highlighting a pair of yellow and blue gears only when they’re actively transferring power directly from the weight gear to the output gear. Only one such pair is active at a time – the other either spins idly, or acts as an intermediate to change the direction of rotation to make sure the output gear always winds the spring.

Notice that the output gear rotates very little relative to the gear attached to the weight, so it takes a lot of arm swinging to fully wind the mainspring. However, over the course of a day the automatic winding mechanism can usually ensure that the mainspring stays wound.

The Size of it All

In all the examples so far we had the comfort of looking at the parts at a fairly large magnification, but in this last demonstration down below you can finally see how tiny all the components are. By dragging the slider you can change the viewing size:

That rounded rectangle surrounding the watch corresponds to the size of a credit card – if you have one handy you can put it on screen and drag the slider until the card fits in that outline. Hopefully, this really puts in perspective how small all the parts we’ve talked about are.

Further Watching and Reading

There are many YouTube channels dedicated to mechanical watches, but I particularly like Wristwatch Revival, which is dedicated to fixing broken watches, which very often involves a complete dissection of a movement, and a repair or replacement of broken parts. Although the creator is not a professional watchmaker, the videos are packed with information and are very enjoyable to watch.

Watchmaking by George Daniels is a book dedicated to the process of actually making watches from scratch. While few will endeavor this journey, the publication also explains many of the considerations required when designing a watch movement and its parts. Many of the book’s pages are accompanied by pretty technical illustrations that help to explain the concepts.

Final Words

In the 1970s mechanical watches started to be dethroned by quartz models, which track time by electronically counting vibrations of a quartz crystal. As technology progressed, typical watches only increased their reliance on digital circuits. Modern smart reincarnations resemble their archetypes only in shape and placement on wrists.

Mechanical watches are not as accurate as digital ones. They require maintenance and are more fragile. Despite all these drawbacks, these devices show a true mastery of engineering. With creative use of miniature gears, levers, and springs, a mechanical watch rises from its dormant components to become truly alive.

Read the whole story
emrox
3 minutes ago
reply
Hamburg, Germany
Share this story
Delete

Your EPUB Is Fine. Kobo Disagrees. Blame Adobe

1 Share

epub shall not pass. If you use html elements out of order, if your document diverges in the slightest from the holy set of rules decreed by the International Digital Publishing

Read the whole story
emrox
1 day ago
reply
Hamburg, Germany
Share this story
Delete

“Don’t You Just Upload It to ChatGPT?”

1 Share

, I ended up cancelling my second class—one of those nights when the first assignment landed in my inbox at 4 p.m., another one arrived while I was on my way to the gym, and a third one popped up right as I was standing in the locker room. All due the following morning,

Read the whole story
emrox
1 day ago
reply
Hamburg, Germany
Share this story
Delete

UI Skills for Design Engineers | Curated Skill Directory

1 Share

Skills for Design Engineers

baseline-ui

Validates animation durations, enforces typography scale, checks component accessibility, and prevents layout anti-patterns in Tailwind CSS projects. Use when building UI components, reviewing CSS utilities, styling React views, or enforcing design consistency.

Ibelick avatar Ibelick

fixing-accessibility

Audit and fix HTML accessibility issues including ARIA labels, keyboard navigation, focus management, color contrast, and form errors. Use when adding interactive controls, forms, dialogs, or reviewing WCAG compliance.

Ibelick avatar Ibelick

fixing-motion-performance

Audit and fix animation performance issues including layout thrashing, compositor properties, scroll-linked motion, and blur effects. Use when animations stutter, transitions jank, or reviewing CSS/JS animation performance.

Ibelick avatar Ibelick

frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Generates creative, polished code and UI design that avoids generic AI aesthetics.

anthropics avatar anthropics

wcag-audit-patterns

Conduct WCAG 2.2 accessibility audits with automated testing, manual verification, and remediation guidance. Use when auditing websites for accessibility, fixing WCAG violations, or implementing accessible design patterns.

wshobson avatar wshobson

emil-design-eng

Emil Kowalski's design-engineering philosophy for UI polish, components, animation, and production-ready frontend craft.

emilkowalski avatar emilkowalski

react-doctor

Run React Doctor to detect regressions in security, performance, correctness, and architecture, with score-based quality checks.

millionco avatar millionco

shadcn

Project-aware shadcn/ui workflow for searching, adding, composing, and fixing components with correct patterns.

shadcn-ui avatar shadcn-ui

make-interfaces-feel-better

Design engineering principles for making interfaces feel polished, with focus on micro-interactions, typography, and visual details.

jakubkrehel avatar jakubkrehel

design-lab

Interactive design exploration workflow: conduct interviews, generate variants, and refine UI designs through feedback.

0xdesign avatar 0xdesign

ui-ux-pro-max

Comprehensive UI/UX design intelligence with 50+ styles, 97 palettes, and 9 technology stacks for building professional interfaces.

nextlevelbuilder avatar nextlevelbuilder

interface-design

Specialized skill for interface design: dashboards, admin panels, and SaaS apps. Focused on craft and consistency.

Dammyjay93 avatar Dammyjay93

12-principles-of-animation

Apply Disney's 12 animation principles to web interfaces to make motion feel natural, organic, and human.

raphaelsalaja avatar raphaelsalaja

impeccable

Flagship design skill for production-grade, anti-generic frontend interfaces with strong craft and consistency.

pbakaus avatar pbakaus

bencium-innovative-ux-designer

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.

bencium avatar bencium

gpt-tasteskill

High-agency UX/UI skill with strict layout variance, typography, and GSAP motion engineering constraints.

Leonxlnx avatar Leonxlnx

frontend-slides

Create animation-rich HTML presentations from scratch or convert PPT/PPTX files into polished web slides.

zarazhangrui avatar zarazhangrui

swiftui-ui-patterns

Best practices and example-driven guidance for building SwiftUI views and components. Includes tab architecture and screen composition.

dimillian avatar dimillian

Read the whole story
emrox
5 days ago
reply
Hamburg, Germany
Share this story
Delete

Voxel Space

1 Share

VoxelSpace

web demonstration

Web Demo of the Voxel Space Engine

History

Let us go back to the year 1992. The CPUs were 1000 times slower than today and the acceleration via a GPU was unknown or unaffordable. 3D games were calculated exclusively on the CPU and the rendering engine rendered filled polygons with a single color.

Game Gunship 2000 in 1991 Game Gunship 2000 published by MicroProse in 1991

It was during that year NovaLogic published the game Comanche.

Game Comanche in 1992 Game Comanche published by NovaLogic in 1992

The graphics were breathtaking for the time being and in my opinion 3 years ahead of its time. You see many more details such as textures on mountains and valleys, and for the first time a neat shading and even shadows. Sure, it’s pixelated, but all games in those years were pixelated.

Render algorithm

Comanche uses a technique called Voxel Space, which is based on the same ideas like ray casting. Hence the Voxel Space engine is a 2.5D engine, it doesn’t have all the levels of freedom that a regular 3D engine offers.

Height map and color map

The easiest way to represent a terrain is through a height map and color map. For the game Comanche a 1024 * 1024 one byte height map and a 1024 * 1024 one byte color map is used which you can download on this site. These maps are periodic:

periodic map

Such maps limit the terrain to “one height per position on the map” - Complex geometries such as buildings or trees are not possible to represent. However, a great advantage of the colormap is, that it already contains the shading and shadows. The Voxel Space engine just takes the color and doesn’t have to compute illumination during the render process.

Basic algorithm

For a 3D engine the rendering algorithm is amazingly simple. The Voxel Space engine rasters the height and color map and draws vertical lines. The following figure demonstrate this technique.

Line by line

  • Clear Screen.
  • To guarantee occlusion start from the back and render to the front. This is called painter algorithm.
  • Determine the line on the map, which corresponds to the same optical distance from the observer. Consider the field of view and the perspective projection (Objects are smaller farther away)
  • Raster the line so that it matches the number of columns of the screen.
  • Retrieve the height and color from the 2D maps corresponding of the segment of the line.
  • Perform the perspective projection for the height coordinate.
  • Draw a vertical line with the corresponding color with the height retrieved from the perspective projection.

The core algorithm contains in its simplest form only a few lines of code (python syntax):

def Render(p, height, horizon, scale_height, distance, screen_width, screen_height):
    # Draw from back to the front (high z coordinate to low z coordinate)
    for z in range(distance, 1, -1):
        # Find line on map. This calculation corresponds to a field of view of 90°
        pleft  = Point(-z + p.x, -z + p.y)
        pright = Point( z + p.x, -z + p.y)
        # segment the line
        dx = (pright.x - pleft.x) / screen_width
        # Raster line and draw a vertical line for each segment
        for i in range(0, screen_width):
            height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
            DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
            pleft.x += dx

# Call the render function with the camera parameters:
# position, height, horizon line position,
# scaling factor for the height, the largest distance, 
# screen width and the screen height parameter
Render( Point(0, 0), 50, 120, 120, 300, 800, 600 )

Add rotation

With the algorithm above we can only view to the north. A different angle needs a few more lines of code to rotate the coordinates.

rotation

def Render(p, phi, height, horizon, scale_height, distance, screen_width, screen_height):
    # precalculate viewing angle parameters
    var sinphi = math.sin(phi);
    var cosphi = math.cos(phi);

    # Draw from back to the front (high z coordinate to low z coordinate)
    for z in range(distance, 1, -1):

        # Find line on map. This calculation corresponds to a field of view of 90°
        pleft = Point(
            (-cosphi*z - sinphi*z) + p.x,
            ( sinphi*z - cosphi*z) + p.y)
        pright = Point(
            ( cosphi*z - sinphi*z) + p.x,
            (-sinphi*z - cosphi*z) + p.y)
        
        # segment the line
        dx = (pright.x - pleft.x) / screen_width
        dy = (pright.y - pleft.y) / screen_width

        # Raster line and draw a vertical line for each segment
        for i in range(0, screen_width):
            height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
            DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
            pleft.x += dx
            pleft.y += dy

# Call the render function with the camera parameters:
# position, viewing angle, height, horizon line position, 
# scaling factor for the height, the largest distance, 
# screen width and the screen height parameter
Render( Point(0, 0), 0, 50, 120, 120, 300, 800, 600 )

More performance

There are of course a lot of tricks to achieve higher performance.

  • Instead of drawing from back to the front we can draw from front to back. The advantage is, the we don’t have to draw lines to the bottom of the screen every time because of occlusion. However, to guarantee occlusion we need an additional y-buffer. For every column, the highest y position is stored. Because we are drawing from the front to back, the visible part of the next line can only be larger then the highest line previously drawn.
  • Level of Detail. Render more details in front but less details far away.

front to back rendering

def Render(p, phi, height, horizon, scale_height, distance, screen_width, screen_height):
    # precalculate viewing angle parameters
    var sinphi = math.sin(phi);
    var cosphi = math.cos(phi);
    
    # initialize visibility array. Y position for each column on screen 
    ybuffer = np.zeros(screen_width)
    for i in range(0, screen_width):
        ybuffer[i] = screen_height

    # Draw from front to the back (low z coordinate to high z coordinate)
    dz = 1.
    z = 1.
    while z < distance
        # Find line on map. This calculation corresponds to a field of view of 90°
        pleft = Point(
            (-cosphi*z - sinphi*z) + p.x,
            ( sinphi*z - cosphi*z) + p.y)
        pright = Point(
            ( cosphi*z - sinphi*z) + p.x,
            (-sinphi*z - cosphi*z) + p.y)

        # segment the line
        dx = (pright.x - pleft.x) / screen_width
        dy = (pright.y - pleft.y) / screen_width

        # Raster line and draw a vertical line for each segment
        for i in range(0, screen_width):
            height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
            DrawVerticalLine(i, height_on_screen, ybuffer[i], colormap[pleft.x, pleft.y])
            if height_on_screen < ybuffer[i]:
                ybuffer[i] = height_on_screen
            pleft.x += dx
            pleft.y += dy

        # Go to next line and increase step size when you are far away
        z += dz
        dz += 0.2

# Call the render function with the camera parameters:
# position, viewing angle, height, horizon line position, 
# scaling factor for the height, the largest distance, 
# screen width and the screen height parameter
Render( Point(0, 0), 0, 50, 120, 120, 300, 800, 600 )

Web Project demo page

Voxel terrain engine - an introduction

Personal website

Maps

color, height

C1W.png D1.png

color, height

C2W.png D2.png

color, height

C3.png D3.png

color, height

C4.png D4.png

color, height

C5W.png D5.png

color, height

C6W.png D6.png

color, height

C7W.png D7.png

color, height

C8.png D6.png

color, height

C9W.png D9.png

color, height

C10W.png D10.png

color, height

C11W.png D11.png

color, height

C12W.png D11.png

color, height

C13.png D13.png

color, height

C14.png D14.png

color, height

C14W.png D14.png

color, height

C15.png D15.png

color, height

C16W.png D16.png

color, height

C17W.png D17.png

color, height

C18W.png D18.png

color, height

C19W.png D19.png

color, height

C20W.png D20.png

color, height

C21.png D21.png

color, height

C22W.png D22.png

color, height

C23W.png D21.png

color, height

C24W.png D24.png

color, height

C25W.png D25.png

color, height

C26W.png D18.png

color, height

C27W.png D15.png

color, height

C28W.png D25.png

color, height

C29W.png D16.png

License

The software part of the repository is under the MIT license. Please read the license file for more information. Please keep in mind, that the Voxel Space technology might be still patented in some countries. The color and height maps are reverse engineered from the game Comanche and are therefore excluded from the license.

Read the whole story
emrox
16 days ago
reply
Hamburg, Germany
Share this story
Delete

Component Architecture for React Server Components

1 Share

For most of React’s history, the conventional way to load data on a page has been to fetch at the top of a route and pass it down through props. Most React developers still reach for that model first, even when working in the Next.js App Router.

In this blog post, we will look at why that habit ends up producing tightly coupled components and clumsy loading states, and explore how React Server Components let us architect a page differently. We will walk through the progression from useEffect to React Query to loaders to RSCs, and then put together a page that describes the loading experience rather than managing all the data.

Table of contents

Background

Data fetching on the server is faster than fetching on the client, for a straightforward reason. When we fetch on the client, we have to wait for the JavaScript bundle to download, parse, and execute before the first request can even fire. As the UI renders and more components mount, each one can trigger its own fetch, leading to waterfalls where requests happen in sequence rather than in parallel. The server, on the other hand, sits next to the database and can fetch in parallel with rendering, sending the result inline with the HTML. The end user gets data without paying for an extra roundtrip.

This is why loaders in frameworks like the old Remix (v1 and v2), the Next.js Pages Router, and more recently React Router v7 and TanStack Router have been so popular. They put data fetching at the route boundary on the server, which is the right place for it. With TanStack Router the loader is actually optional, and a common setup is to combine it with TanStack Query, where each component still uses its own useQuery for data and the loader only kicks off prefetching for that data on the route. That’s arguably a nicer split, because we keep component-local fetching while still getting the route-level head start.

The question is what we lose in the process, and whether RSCs let us keep the server-side wins without the trade-offs. For a deeper look at the performance side of this, Nadia Makarevich’s article React Server Components: Do They Really Improve Performance? is a great companion to this post. She measures the same app across CSR, SSR with loaders, and RSCs, and shows that the real performance gains only land once we rewrite data fetching to be server-first and add deliberate Suspense boundaries.

The Use Case

For the rest of this post, let’s imagine we are building a social feed page. The UI has a sidebar, a feed of posts, a list of suggested users to follow, and a list of trending tags. In plain JSX, the page looks something like this:

function HomePage() {
  return (
    <Layout>
      <Sidebar />
      <main>
        <PageHeader title="Home" />
        <Feed />
      </main>
      <aside>
        <TrendingTags />
        <WhoToFollow />
      </aside>
    </Layout>
  );
}

This is just the layout. No data yet, no fetching, no loading states. Every component here will eventually need data, but right now we are just describing what the page looks like. From here, we can explore how different approaches to data fetching change the shape of this page.

1. Local Data Fetching

The original way to handle data in React was with useEffect and useState. Each component fetches its own data, owns its own loading flag, and lifts state up when something else needs to know:

function Feed() {
  const [posts, setPosts] = useState<PostT[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchFeed().then(p => {
      setPosts(p);
      setIsLoading(false);
    });
  }, []);

  if (isLoading) return <FeedSkeleton />;
  return <ul>{posts.map(post => <Post key={post.id} post={post} />)}</ul>;
}

This works for a single component, but the moment another part of the tree needs the same data, we have to hoist posts and setPosts up to a common ancestor and pass them down. Mutations follow the same pattern: if Post wants to like itself and have the count update elsewhere, the like handler has to live somewhere both components can reach, which is usually higher than either of them needs to be. We end up lifting state for reasons that have nothing to do with the UI structure:

function HomePage() {
  const [posts, setPosts] = useState<PostT[]>([]);
  // ...fetch logic...

  function handleLike(postId: string) {
    likePost(postId).then(() => {
      fetchFeed().then(setPosts);
    });
  }

  return (
    <Feed posts={posts} onLike={handleLike} />
  );
}

function Feed({ posts, onLike }: Props) {
  return (
    <ul>
      {posts.map(post => (
        <Post key={post.id} post={post}>
          <LikeButton onClick={() => onLike(post.id)} />
        </Post>
      ))}
    </ul>
  );
}

The like handler lives in HomePage because it needs to update posts. Feed receives both the data and the callback. LikeButton has no idea where the handler comes from. Everything flows through props.

React Query and similar libraries cleaned this up significantly. The data is keyed and cached centrally, so any component can ask for it without prop drilling, and mutations can invalidate or update entries from anywhere:

function Feed() {
  const { data, isLoading } = useQuery({ queryKey: ["feed"], queryFn: fetchFeed });
  if (isLoading) return <FeedSkeleton />;
  return <ul>{data.map(post => <Post key={post.id} post={post} />)}</ul>;
}

function LikeButton({ postId }: { postId: string }) {
  const qc = useQueryClient();
  const mutation = useMutation({
    mutationFn: () => likePost(postId),
    onSuccess: () => qc.invalidateQueries({ queryKey: ["feed"] }),
  });
  return <button onClick={() => mutation.mutate()}>Like</button>;
}

Suddenly the state doesn’t need to live higher than the component that owns it. LikeButton can sit deep in the tree, fire its mutation, and the Feed query refetches without anyone above either component knowing. This is genuinely better, and a big part of why React Query has the position it does.

The downside, in either case, is that every component decides when it is ready independently, and we end up with popcorn UI: things pop into the page one by one in whatever order the network happens to return them. We haven’t designed a loading sequence, we have outsourced it to the network. On top of that, all of this fetching happens on the client, so the user has to wait for the JavaScript to download and execute before any data request even starts. Loaders were an attempt to solve this.

2. Route-Level Loaders

To fix the client-fetching problem, we can move the data fetching to the server with a route-level loader. Instead of each component fetching on its own, a single function fetches everything the page needs up front, and the result is passed down to the page component. In React Router, that looks like this:

// React Router / Remix style
export async function loader() {
  const user = await getCurrentUser();
  const [feed, whoToFollow, trendingTags] = await Promise.all([
    getFeed(user.handle),
    getWhoToFollow(user.handle),
    getTrendingTags(),
  ]);
  return { user, feed, whoToFollow, trendingTags };
}

export default function HomePage() {
  const { user, feed, whoToFollow, trendingTags } = useLoaderData<typeof loader>();

  return (
    <Layout>
      <Sidebar user={user} />
      <Feed posts={feed.posts} currentUser={user} />
      <aside>
        <TrendingTags tags={trendingTags} />
        <WhoToFollow users={whoToFollow} currentUser={user} />
      </aside>
    </Layout>
  );
}

The equivalent in the old Next.js Pages Router would be getServerSideProps, which passes the data as props to the page. Either way, the loader sits at the route boundary and the components below it receive concrete data shapes. Notice that mutations like the LikeButton from earlier are no longer visible at the page level: the loader only handles reads, and writes typically go through separate API calls or form submissions that trigger a page reload or revalidation.

The same mindset is easy to recreate at the page component level in the Next.js App Router. We just make the page itself async and await everything at the top:

// Next.js App Router, loader mindset
export default async function HomePage() {
  const user = await getCurrentUser();
  const [feed, whoToFollow, trendingTags] = await Promise.all([
    getFeed(user.handle),
    getWhoToFollow(user.handle),
    getTrendingTags(),
  ]);

  return (
    <Layout>
      <Sidebar user={user} />
      <Feed posts={feed.posts} currentUser={user} />
      <aside>
        <TrendingTags tags={trendingTags} />
        <WhoToFollow users={whoToFollow} currentUser={user} />
      </aside>
    </Layout>
  );
}

The framework is different, but the shape is identical. The page is still the data owner, and the components are still views that receive whatever the page chose to fetch.

This feels organized, but the components are now coupled to whatever the page chose to fetch for them. Our WhoToFollow component just renders whatever it receives:

function WhoToFollow({ users, currentUser }: Props) {
  return (
    <ul>
      {users.map(user => (
        <UserRow key={user.handle} user={user} currentUser={currentUser} />
      ))}
    </ul>
  );
}

On the home page, it works fine because the page already fetches whoToFollow. But now we want to reuse it on a profile page too:

// home page
const [user, feed, whoToFollow] = await Promise.all([
  getCurrentUser(),
  getFeed(/* ... */),
  getWhoToFollow(/* ... */),
]);

// profile page (now needs the same thing)
const [user, profile, whoToFollow] = await Promise.all([
  getCurrentUser(),
  getProfile(handle),
  getWhoToFollow(/* ... */), // duplicated
]);

<WhoToFollow users={whoToFollow} currentUser={user} />;

The component itself didn’t change, but every new route that wants to use it has to fetch the same data, in the same shape, and thread the same props through every wrapper above it. The component is essentially welded to whichever loader happens to be fetching its data. This is inherent to the loader pattern in any framework: the data lives at the route boundary, and everything below it is a view that receives props.

3. Async Server Components

What if each component could fetch its own data on the server, without needing a loader to hand it down? That is exactly what React Server Components enable. They can be async, they run on the server, they can read from the database directly, and they never execute in the browser. This is what lets us keep the composability of the useEffect approach while still fetching on the server like with loaders: each component owns its data, but the fetch happens during server rendering and the result is sent to the client as rendered HTML.

The Next.js App Router is where most developers encounter RSCs today, and it makes this the default: every component is a server component unless we explicitly mark it with "use client".

Instead of the page fetching everything and passing it down, each component fetches what it needs based on minimal props, usually just an identifier. The component is self-contained: the consumer passes the minimum it needs to know (often just an ID or a handle), and the component resolves whatever else it requires internally. Let’s take WhoToFollow from the loader example. As a server component, it doesn’t need the users and currentUser props the page was handing it. It can resolve the current user and fetch the list itself:

export async function WhoToFollow() {
  const handle = await getCurrentUserHandle();
  const users = await getWhoToFollow(handle);
  return (
    <ul>
      {users.map(user => (
        <UserRow key={user.handle} handle={user.handle} />
      ))}
    </ul>
  );
}

Now we can use <WhoToFollow /> on any page without wiring up the data from above. The same component that needed two separate loaders earlier just works.

The same applies to Feed. In the loader version, it received posts and currentUser as props. As a server component, it fetches its own data and renders the list of posts directly:

export async function Feed() {
  const handle = await getCurrentUserHandle();
  const { posts } = await getFeed(handle);
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Post post={post} />
        </li>
      ))}
    </ul>
  );
}

The page just renders <Feed />. Notice that Feed passes the whole post object down to Post. We already have the data from the feed query, so there is no reason for each Post to refetch its own row by id.

With every component fetching its own data, the page itself goes back to looking like this:

export default function HomePage() {
  return (
    <Layout>
      <Sidebar />
      <main>
        <PageHeader title="Home" />
        <Feed />
      </main>
      <aside>
        <TrendingTags />
        <WhoToFollow />
      </aside>
    </Layout>
  );
}

The structure is the same as the use case. The difference is that every component in this tree is now fetching its own data on the server.

You might be worried about duplicate fetches at this point. With each component fetching its own data, the same getCurrentUserHandle could be called from multiple places in the same render. React’s cache() function deduplicates these per request, so calling it ten times in the same render hits the source once. This is similar to what React Query’s centralized cache does on the client, but built into the server render itself. I covered this in more depth in my previous post on Avoiding Server Component Waterfall Fetching with React 19 cache().

cache() handles deduplication, but not batching. If many components on a page each fetch their own data by a different identifier, you can end up making a lot of separate requests that could have been one. See @_mjmeyer’s answer for more on this.

This composability is also why AI coding agents work so well with React in general, and RSCs extend that composability model to the server. A self-contained component can be moved to a new page, reused in a different layout, or refactored without touching anything outside its own file. The agent doesn’t need to trace data through loaders or prop chains to understand what a component needs.

Building the App in Next.js

Now that we have components that fetch their own data and can be reused across pages without loaders, the next question is: how do we build real apps with this? Let’s say our social feed app has more than one page:

app/
  layout.tsx            // root shell: nav, sidebar
  page.tsx              // home feed
  explore/
    page.tsx            // discover feed
  post/
    [id]/
      page.tsx          // single post with replies

A component like <WhoToFollow /> works on any of these pages without the page having to fetch anything for it. The page is free to focus on what the user actually sees while things load.

From this point on, the layout markup and the sidebar live in app/layout.tsx so they wrap every page automatically.

Avoiding Blocking Renders

Server components render on the server as a stream, which means React can start sending HTML to the client before every async component has finished fetching. Suspense is what makes this work. Wrapping an async component in a Suspense boundary with a fallback tells React to send the fallback immediately while the component resolves in the background. Once it is ready, React streams the real content in and swaps it into place:

<Suspense fallback={<FeedSkeleton />}>
  <Feed />
</Suspense>

Without Suspense, the page waits for every async component to finish before sending anything. Adding a boundary is how we avoid that, and it is also what unlocks the real performance gains that Nadia’s article measures.

Making Skeletons That Stay in Sync

The fallback we pass to Suspense is what the user sees while an async component is fetching. Usually this is a skeleton: a lightweight placeholder that matches the shape of the content it stands in for, with the same dimensions and layout but no real data. Sometimes a spinner is enough instead. Either way, it is just HTML and CSS, and the goal is to avoid layout shift when the real content arrives.

To keep the skeleton in sync with its component, I like to export both from the same file:

// features/post/components/feed.tsx
export async function Feed() {
  const handle = await getCurrentUserHandle();
  const { posts } = await getFeed(handle);
  return (
    <ul className="flex flex-col gap-4">
      {posts.map(post => (
        <li key={post.id}>
          <Post post={post} />
        </li>
      ))}
    </ul>
  );
}

export function FeedSkeleton({ count = 5 }: { count?: number }) {
  return (
    <ul className="flex flex-col gap-4">
      {Array.from({ length: count }).map((_, i) => (
        <li key={i}>
          <PostSkeleton />
        </li>
      ))}
    </ul>
  );
}

PostSkeleton can use the same outer classes as Post so the box reserves the same space, with pulsing rectangles standing in for the text:

// features/post/components/post.tsx
export function Post({ post }: { post: PostT }) {
  return (
    <article className="h-24 rounded-lg border p-4">
      <p>{post.body}</p>
    </article>
  );
}

export function PostSkeleton() {
  return (
    <article className="h-24 rounded-lg border p-4">
      <div className="h-4 w-32 animate-pulse rounded bg-muted" />
    </article>
  );
}

Notice that FeedSkeleton is composed from PostSkeleton, the same way Feed is composed from Post. The skeletons mirror the component tree. When we edit Post to add a new line of metadata or change the avatar size, PostSkeleton is right there in the same file. Drift between the loading state and the rendered state, which is the most common cause of layout jank, gets caught at the time the change is made instead of in a QA pass later. An AI coding agent editing the component will see it too and remember to update the skeleton to match. When we compose a page, we know where to find the right fallback shape for each component.

Designing the Loading Experience

With Suspense and skeletons in place, the question becomes: how do we want the page to load? We could wrap the entire content area in a single boundary:

// app/page.tsx
export default function HomePage() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <main>
        <PageHeader title="Home" />
        <Feed />
      </main>
      <aside>
        <TrendingTags />
        <WhoToFollow />
      </aside>
    </Suspense>
  );
}

The sidebar shows up immediately. Everything else waits behind one boundary and appears at once. Simple, but the user stares at a single skeleton until the slowest component finishes.

Or we can split the boundaries so that each section streams independently:

// app/page.tsx
export default function HomePage() {
  return (
    <>
      <main>
        <PageHeader title="Home" />
        <Suspense fallback={<FeedSkeleton />}>
          <Feed />
        </Suspense>
      </main>
      <aside>
        <Suspense fallback={<TrendingTagsSkeleton />}>
          <TrendingTags />
        </Suspense>
        <Suspense fallback={<WhoToFollowSkeleton />}>
          <WhoToFollow />
        </Suspense>
      </aside>
    </>
  );
}

Now the layout shell stays static, the page header sits outside any boundary, and the feed, follow suggestions, and trending tags each resolve on their own. If the suggestions are fast and the feed is slow, the user sees suggestions first. This can also feel fragmented: three separate regions popping in at different times is not always a better experience.

We could also group the aside behind a single boundary:

// app/page.tsx
export default function HomePage() {
  return (
    <>
      <main>
        <PageHeader title="Home" />
        <Suspense fallback={<FeedSkeleton />}>
          <Feed />
        </Suspense>
      </main>
      <Suspense fallback={<TrendingTagsSkeleton />}>
        <aside>
          <TrendingTags />
          <WhoToFollow />
        </aside>
      </Suspense>
    </>
  );
}

The page now loads in two groups instead of three. Notice that the fallback is only <TrendingTagsSkeleton />. TrendingTags can return a variable number of items, so we don’t know how tall it will be. If we also showed a <WhoToFollowSkeleton /> below it, the skeleton would likely be at the wrong vertical position once the real trending tags resolve. By only showing the trending tags skeleton, we avoid that mismatch. The entire aside appears at once when both components are ready.

When every component manages its own loading state on the client, the page has no say in what appears when. With Suspense, the page decides where the user waits. There is no formula for the perfect boundary placement; it comes down to trying different groupings, seeing how they feel, and iterating.

Notice how readable the page is at this point. We can look at the JSX and see exactly what renders, what shows a skeleton, and what is part of the static shell.

Modern loaders can stream too. In React Router v7, returning a promise from a loader lets that data resolve behind a Suspense boundary while the rest of the route renders. The page still receives the data as props through useLoaderData, though, so we are back to passing data down from the route boundary, which is what we are trying to avoid here.

Building a Parameterized Page

Our route tree also has a parameterized route at post/[id]/page.tsx. This page renders a single post with its replies underneath. PostDetail takes an id, fetches the post itself, and can use the same building blocks as the Post list item from the feed.

In the Next.js App Router, params is a Promise (since Next.js 15), so we need to resolve it. We could await it at the page level:

// app/post/[id]/page.tsx
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <div>
      <PageHeader title="Post" />
      <Suspense fallback={<PostDetailSkeleton />}>
        <PostDetail id={id} />
        <section>
          <SectionHeader>Replies</SectionHeader>
          <Suspense fallback={<RepliesSkeleton />}>
            <Replies postId={id} />
          </Suspense>
        </section>
      </Suspense>
    </div>
  );
}

This works, but it makes the page async, which means it has to wait for params to resolve before rendering anything. One option is to extract a small async component whose only job is to read params:

async function PostContent({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <>
      <PostDetail id={id} />
      <section>
        <SectionHeader>Replies</SectionHeader>
        <Suspense fallback={<RepliesSkeleton />}>
          <Replies postId={id} />
        </Suspense>
      </section>
    </>
  );
}

That keeps the page synchronous, but adds a wrapper component just to unwrap a Promise. Instead, we can use .then() directly:

// app/post/[id]/page.tsx
export default function PostPage({ params }: { params: Promise<{ id: string }> }) {
  return (
    <div>
      <PageHeader title="Post" />
      <Suspense fallback={<PostDetailSkeleton />}>
        {params.then(({ id }) => (
          <>
            <PostDetail id={id} />
            <section>
              <SectionHeader>Replies</SectionHeader>
              <Suspense fallback={<RepliesSkeleton />}>
                <Replies postId={id} />
              </Suspense>
            </section>
          </>
        ))}
      </Suspense>
    </div>
  );
}

The .then() resolves params so that PostDetail and Replies still receive a plain id string as a prop, and the page stays synchronous and readable. The same trick works for searchParams, which is also a Promise in Next.js 15+. This pattern also sets us up nicely for cache components later, where keeping pages synchronous matters even more.

The loading sequence follows the same thinking as on the home feed: the header is part of the static shell, the post detail streams in behind a Suspense boundary, and Replies has its own boundary inside so it can resolve independently from the post.

Adding Interactivity

The feed itself might have interactive parts: the like button on every post needs JavaScript on the client. Client components can compose the same way. Here is a LikeButton that uses a form action to call a Server Function (likePost), with useOptimistic for instant feedback:

'use client';

export function LikeButton({ postId, liked, count }: Props) {
  const [optimistic, setOptimistic] = useOptimistic({ liked, count });

  const likeAction = async () => {
    setOptimistic({
      liked: !optimistic.liked,
      count: optimistic.count + (optimistic.liked ? -1 : 1),
    });
    await likePost(postId);
  };

  return (
    <form action={likeAction}>
      <Button>{optimistic.liked ? "" : ""} {optimistic.count}</Button>
    </form>
  );
}

The form calls likePost directly across the server boundary, and useOptimistic updates the UI before the server responds.

useOptimistic is local to the component that uses it. If the update only affects the local component, that is enough. When another part of the page needs to react to the same update (a follower count, a notification badge), we can either lift the optimistic state into a context or let the framework revalidate.

Every Post in the feed composes it alongside the rest of the server-rendered content:

// features/post/components/post.tsx
export async function Post({ post }: { post: PostT }) {
  const userState = await getPostUserState(post.id);
  return (
    <article>
      <PostAuthor handle={post.authorHandle} />
      <PostBody body={post.body} />
      <LikeButton postId={post.id} liked={userState.liked} count={post.likes} />
    </article>
  );
}

My previous blog posts on server and client component composition in practice and building design components with action props using async React cover the client side of this in more depth.

Organizing the Codebase

When components are this self-contained, it becomes natural to group them by feature. A feature folder structure works well for this:

features/
  post/
    components/
      post.tsx                   // server component + skeleton
      post-detail.tsx            // server component + skeleton
      feed.tsx                   // server component + skeleton
      like-button.tsx            // client component
      replies.tsx                // server component + skeleton
  user/
    components/
      user-avatar.tsx            // server component + skeleton
      who-to-follow.tsx          // server component + skeleton

Because our components only accept minimal props like an identifier and fetch their own data, they can be picked up and composed into any page. <UserAvatar handle={handle} /> is a good example: the same component renders in the feed, on a post’s author row, next to each reply, in the follow suggestions, and in the sidebar, with nothing more than a handle. Refactoring a component to a new page doesn’t touch anything outside its feature folder.

Feature slicing is just one way to organize this. Any structure works as long as the components stay self-contained, but the reusable model maps especially well to feature folders.

Along the same lines, we can also add error handling and animations to a region by wrapping it in a React ErrorBoundary (in Next.js, catchError gives us a retry button on top of that, which I covered in Error Handling in Next.js with catchError) or in a ViewTransition to animate the content as it streams in. The page composes them around its async components.

Pulling everything from this post into one place, the home feed page might end up looking something like this:

// app/page.tsx
export default function HomePage() {
  return (
    <>
      <main>
        <PageHeader title="Home" />
        <ErrorBoundary title="Failed to load feed">
          <Suspense fallback={<FeedSkeleton />}>
            <ViewTransition>
              <Feed />
            </ViewTransition>
          </Suspense>
        </ErrorBoundary>
      </main>
      <ErrorBoundary title="Failed to load suggestions">
        <Suspense fallback={<TrendingTagsSkeleton />}>
          <aside>
            <TrendingTags />
            <WhoToFollow />
          </aside>
        </Suspense>
      </ErrorBoundary>
    </>
  );
}

Each region has its own error boundary, so a failure in one part of the page doesn’t take down the rest. The ViewTransition around the feed animates the content into place as it streams in, so the swap from skeleton to real posts feels smooth instead of abrupt.

The post detail page can follow the same pattern, with a view transition and an inner error boundary around replies so they can fail independently of the post. The outer route-level error can be handled by Next.js’s error.tsx, so we don’t need to wrap the whole page ourselves:

// app/post/[id]/page.tsx
export default function PostPage({ params }: { params: Promise<{ id: string }> }) {
  return (
    <div>
      <PageHeader title="Post" />
      <Suspense fallback={<PostDetailSkeleton />}>
        <ViewTransition>
          {params.then(({ id }) => (
            <>
              <PostDetail id={id} />
              <section>
                <SectionHeader>Replies</SectionHeader>
                <ErrorBoundary title="Failed to load replies">
                  <Suspense fallback={<RepliesSkeleton />}>
                    <Replies postId={id} />
                  </Suspense>
                </ErrorBoundary>
              </section>
            </>
          ))}
        </ViewTransition>
      </Suspense>
    </div>
  );
}

A Note on Cache Components

With cacheComponents enabled in Next.js 16, any component that fetches dynamic data has to live behind a Suspense boundary. Everything outside those boundaries becomes part of the static shell that can be prerendered and served instantly. This enables Partial Prerendering: the static parts are served immediately, and the dynamic parts stream in. With 'use cache', we can also cache individual components or data fetches, which means some regions that previously needed a Suspense fallback can resolve instantly and the loading states disappear entirely.

The architecture we have been building throughout this post fits naturally into this model: components fetch their own data, pages place deliberate boundaries, and we choose what shows up immediately versus what streams. The .then() pattern we used on the parameterized page matters even more here, because awaiting params at the page level would pull the entire page out of the static shell and cause an error.

Building this way from the start pays off even before we turn on cacheComponents. Once we do, the architecture is already in place.

Conclusion

The trip from useEffect to React Query to loaders to RSCs has really been about getting data fetching to the server while keeping components composable. RSCs are not the only way to get there, but they compose beautifully with React’s component model, and Suspense gives us a way to design the loading experience on top of that.

If you are still reflexively writing async function Page and awaiting five queries at the top, try the inversion. Many of us learned that habit from loaders and getServerSideProps, and AI coding agents have been trained on the same patterns. Push the data fetches into the components that use them, and let Suspense handle the orchestration. The result is a codebase that is easier to read, easier to move around in, and easier for both humans and agents to work with.

To summarize the principles:

  • Pages are synchronous compositors. They don’t fetch, they compose.
  • Async components fetch their own data. Co-locate the read with the JSX.
  • Skeletons live next to their component. Same file, exported alongside it.
  • Suspense boundaries go at the page level. The page designs the loading sequence.
  • Client boundaries are leaf nodes. Push 'use client' as deep as it can go.

The demo app from this post is open source at next16-social-media if you want to explore the full codebase.

I hope this post has been helpful. Thanks to Nadia Makarevich for benchmarking RSC performance in her article, so you don’t have to take my word for it. Please let me know if you have any questions or comments, and follow me on Bluesky or X for more updates. Happy coding! 🚀

Read the whole story
emrox
18 days ago
reply
Hamburg, Germany
Share this story
Delete
Next Page of Stories