3901 stories
·
3 followers

What will be left for us to work on?

1 Share

I had the honor of giving a keynote at the International Conference on Machine Learning in Seoul last week titled “What will be left for us to work on?” I addressed the widespread anxiety about how we should adapt as AI capabilities increase. I was thrilled by the talk’s reception, so I have made my slides available here, annotated with a lightly edited transcript. You can also view them below right here on this page, but the online version has animations, clickable links, and a much nicer experience overall.

I made three arguments. First, the AI as Normal Technology framework is a correct and useful as a way to think about AI’s impacts, unless and until there is some future discontinuity such as through recursive self-improvement. Second, even though we should take recursive self-improvement seriously, there is no milestone that companies might achieve in the lab that will suddenly put us all out of work. Third and finally, jobs of the future will be radically different, and a lot of adaptation will be needed. I shared my thinking about what this might look like and ended with a vision of human/AI “co-superintelligence”.

Now is a time of great excitement in AI, but it’s also a time of great anxiety in the AI community. I want to address that anxiety head on. How do we prepare for a future where AI will become capable of doing more and more of the work that we do today?

I lead a team at Princeton University trying to advance the science of AI agent evaluation. We try to go beyond the usual claims of “Look, capability is going up on benchmarks!” Those claims tend to be misinterpreted by the broader public as implying that agents are soon about to take all our jobs.

Maybe that will happen. But in our work we try to understand the factors beyond capability that matter for real-world deployment, and bring that understanding into evaluations.

The work that I’m better known for is the essay I co-authored with Sayash Kapoor called AI as Normal Technology. It’s a way to think about the medium-term future of AI and how to adapt to it — and in turn how to adapt it to the needs of society and the economy.

So we’ve been going around writing these essays about how lawyers should adapt, or maybe how journalists should adapt. But perhaps ironically, the question of how to adapt has been hitting our community first. Whether it’s software engineering or AI research itself, AI capabilities in these areas are of course advancing very rapidly.

Our response to this moment matters beyond this community. The whole world is watching. If we simply roll over and accept that a lot of our work will be done by AI in the future, instead of setting clear boundaries, I think it will lead to an even stronger political backlash against AI than what we are seeing today. So I think this question is not just for us but for the whole world.

From the beginning of AI, historically there have been these two battling narratives. In the past, the distinction was academic and philosophical, but now it has become an acutely practical question. Each one of us has to decide which camp we’re in, or where on this spectrum we’re in, because the practical consequences of believing in one versus the other are very, very different.

If you think this is a technology which in a few years is going to be able to replace everything we do today, then perhaps the correct response is to build wealth as quickly as possible before our skills become irrelevant. And this is the path that many have chosen in Silicon Valley. You may have heard of the “permanent underclass” meme.

On the other hand, if you believe, as I do, that this is a technology that will greatly amplify our potential, then now is the best time to build skills — especially the skills that are going to be complementary to what AI is doing and is going to be able to do — as well as to build all the things around it, such as agency and taste and judgment.

If you choose the first path, and it turns out that AI actually ends up being an amplifying technology as opposed to a replacing technology, then I would argue that over the next few years you’ve perhaps lost the best time in history to build these skills that will give us superpowers. That’s why we all need to think about this question, even if we won’t all land in the same place.

AI as Normal Technology is the intellectual framework for my talk today. When we say AI is normal, we don’t mean that it’s just like a hammer or a toothbrush, some kind of mundane technology.

We acknowledge prominently in the essay that this is a transformative technology on the scale of the industrial revolution. We’re not AI skeptics.

This is not a slogan. It’s a framework — sort of a causal model how AI capabilities impact the economy and society. It’s a 15,000-word essay and we’re turning it into a book. And I mention that because people often hear the word normal and they assume they know what we mean, but that leads to misunderstandings.

First, I’ll argue that this framework is correct and useful as a way to think about AI’s impacts, unless and until there is some future discontinuity — such as through recursive self-improvement — that leads to future impacts looking very different from past impacts.

Second, I will talk about why, even though we should take recursive self-improvement seriously, I’m not particularly losing sleep over it.

Third and finally, I want to be clear that I’m not saying that jobs of the future will be just like jobs of the present. A lot of adaptation will be needed. So I want to give some preliminary thinking on how I think our roles are going to change and how we can best adapt to the changes.

Powerful technologies of the past, like electricity, have been thoroughly studied, and we have good frameworks to understand how technological progress leads to economic impacts.

Invention: discovering the principles of electromagnetism, AC versus DC, etc.

Innovation: People don’t use “electricity” directly. We use electrical appliances. Those had to be invented, so that is a kind of downstream innovation — that’s the second phase of the framework.

Diffusion: This refers to the gradual process by which people start adopting innovations.

In our essay we apply this framework to AI and flesh it out into a four-part framework.

Here’s the basic picture, illustrated with software engineering as an example.
Methods/capabilities: Models are rapidly improving.
Products/applications: We don’t use LLMs directly. The reason they’ve been so influential in all of our work is because of coding agents. These are products that take those latent capabilities and turn them into something useful and usable for workers.
Early adoption: At first people were mostly trying vibe coding, and now we know that that’s not really the best way to develop production software — so now we have more sophisticated ways of doing agentic engineering.
Adaptation: (or structural transformation) — the fourth and slowest phase. Much of my talk today is going to be about that. I claim that this stage takes decades. It has not really started yet, even in a field like software engineering, which is a relative early adopter of coding agents.

We don’t know what the adaptation phase will look like — we can only speculate. Permit me to speculate for a minute. If it’s going to be the case that coding agents are going to be able to create ten-million-line code bases in the future that are not full of bugs and security vulnerabilities, then it won’t make a lot of sense for us to create one piece of software that billions of people should use. It’ll make a lot more sense for software to be tailored to the needs of each individual or team. And that’s what I mean by extreme personalization.

It’s not merely a technological change — that’s also a change for the industry. For instance: do we even need software companies anymore? Maybe software development will massively shift in-house, into the companies and teams that are actually using the software. Again, this is speculation, but the point is that it is this kind of organizational change, human change — that’s very slow, that takes decades — that will allow us to take advantage of the full potential of AI, whether it’s in software engineering or in any other field. So that’s one of the central insights of the essay. When we look at past technologies, this kind of change tends to be very slow.

Before electricity, factories used to look like the picture on the left. A massive steam engine generated power and it was moved throughout the factory by mechanical gears and belts. So when electricity came along, factory owners tried to replace those steam boilers with electric generators. They thought it would be much more efficient. But this idea of a drop-in replacement did not work. We keep hearing that term in the context of AI agents today — that they will be drop-in replacements for human workers. That did not work in the case of electricity.

What actually worked, and what took 40 years to develop, is to recognize that electricity is a very different technology. It’s portable, so you can move the power to wherever you need it. That lets you reorganize the entire layout of the factory around the logic of the assembly line. And that required changing the way that workers are trained, hired, and fired, new labor laws, and so forth. So that’s the kind of organizational adaptation that it took in order to reap the benefits of electricity in factories.

Our claim is that this is the kind of process that we will go through for AI. A couple of decades from now, we will have fundamentally reorganized work. We don’t know what that’s going to look like, and that is the challenge in front of all of us. And that’s not just a job for the AI companies to do, much like it wasn’t the job of the electric utility to figure out how factories should be reorganized. In our view, this is the slowest of the four stages through which AI leads to economic impacts. Today, this process has not really gotten started.

Why is there a huge gap between what people in various occupations could be using AI for and what they’re actually using it for? One reason could be that people are slow to adopt technology, and that’s certainly part of our framework.

But we wondered if maybe the people who are deploying AI and are not having much success at it know something about the practical limitations of AI that the AI industry doesn’t. Let’s have a bit more humility about the relationship between capabilities and deployment.

Given that the #1 concern people cite is reliability, we wanted to try to measure whether reliability, distinct from capability, is a barrier to the practical usefulness of AI agents.

We looked at 10-12 reliability metrics and clustered them into four dimensions.
Consistency: Suppose we hear that an AI agent has a 70% accuracy. Does this mean it works on 70% of the tasks, but on the ones that it does, it does so every time? That’s great for deployment — you can deploy it on that subset of the tasks. Or does it mean that on any given task it might unpredictably fail with a 30% probability? That’s pretty useless from a deployment perspective. Perhaps shockingly, none of the agent benchmarks that we looked into make a distinction between these two. Both of these are represented as 70% accuracy.
Robustness: We looked at robustness: what happens when the environment changes a little bit?
Calibration: Can the agent look back at its transcript and tell if it performed the task correctly?
Operational safety: When it does fail, is it recoverable, or is it something like deleting the production database?

For a human worker, if we think of someone as being competent at a job, it’s all of these things, not just accuracy. But it turns out we were measuring agents only on accuracy.

We measured capability and reliability using two complementary benchmarks, for models from these 3 frontier AI companies that were released over the last 24 months or so.

This is a period during which accuracy or capability shot up dramatically (left).

But reliability (right) only increased by five or ten percentage points.

There are a bunch of implications but let me highlight one. Right now the industry is not treating automation and collaboration agents differently. If you run an agent in headless mode, I guess that becomes an automation agent. That is not a good way to look at it, because properties like reliability that are very important for automation agents can actually be a hindrance for a collaboration agent that you might be using to improve your creative writing or something like that. For that kind of agent, you don’t want it to behave like a robot that does the same thing every time. You want it to be creative and explore different possibilities and be unpredictable.

Scaffolds and even the post-training of models should be different based on whether it is supposed to be driving a collaboration agent or an automation agent.

My hope is that reliability will continue to improve and automation will become easier over time. But for now I think collaboration agents will continue to be much more successful. Many companies that hastily rushed to automate business processes using agents are recognizing the limits and costs, and even legal liabilities — like when an agent deletes production data.

For the time being, you can have only two out of these three properties in agents: general-purpose (a language model based agent that can be instructed to do different tasks rather than purpose-built for a task like traditional software); deployed in high-stakes scenarios, and automated.

This is one of the reasons that I think that for now, AI remains much more of a collaboration technology than a technology that automates workers away.

Let’s take software engineering as a case study. It’s a good leading indicator because coding agents have been particularly rapidly adopted.

You might think: okay, we can’t completely automate away software engineering. But if agents make software engineers ten times more productive, then we need ten times fewer software engineers. Isn’t that an obvious consequence?

Well, that is completely contradicted by the data. We looked at this in a follow-up essay. In every case we looked at, the company was under financial pressure, and it turns out to be more convenient to blame AI for the layoffs instead.

Why is AI not replacing software engineers so far? We’ve known for a while — this is a paper from 2019 — that writing code is not really the bottleneck.

Over the last year, as software engineers started adopting coding agents and started to recognize that it doesn’t seem to be cutting down on the amount of their work, there have been many blog posts rediscovering the fact that writing code is not a bottleneck. Here is a small sample.

So what actually is the bottleneck?

This framework is our answer to the question.

The decide layer: understanding customer requirements, developing the specification, planning, etc. That is not getting compressed by AI.

The execute layer: the actual coding and debugging. This is getting compressed, but it was only maybe one-third of the work to begin with.

The deliver layer: Understanding your code deeply enough to be accountable for what you release; carrying out integration into customer systems, maintenance, testing, etc. This layer is not getting compressed either.

In fact, the first and third layers are arguably expanding as AI compresses the middle layer — and I’ll come back to that point.

I think it is already the case in software engineering, and will increasingly be the case in many professions, that we can think of knowledge workers as similar to a crane operator or a forklift operator. The machine greatly amplifies the human potential to do physical work — it’s doing all the heavy lifting, but the person still remains in control. And I think this is what is happening with cognitive work. Machines are going to increasingly do the cognitive heavy lifting, but the person still remains in control.

The entire job gets reconceptualized as being about operating the machine, understanding the machine, and controlling the machine, as opposed to doing the cognitive work ourselves.

All this might seem like a dramatic change, but in a sense it is only a continuation of what has been happening over and over and over in software engineering. Starting from the days of machine code, we’ve had many waves of technology, each of which gives us nearly an order of magnitude increase in productivity. And far from decreasing the demand for software engineers, during the time that we’ve climbed up this ladder, the amount of software engineering employment has increased by a factor of something like 10,000. And that’s simply because the amount of code that there is to write has gone up by orders and orders of magnitude.

Economists have found this repeatedly. You might have heard the term Jevons’ paradox; I like the term “lump-of-labor fallacy”.

ATMs made it economically feasible for banks to open lots of regional branches. And those branches still needed human tellers to handle the things ATMs couldn’t. So, paradoxically, employment actually grew.

Geoff Hinton made the famous prediction a decade ago that radiologists would be basically extinct in five years. But it turns out radiology employment has in fact grown. And it’s not because radiologists are rejecting AI. They are actually enthusiastically adopting it. One reason for job growth is that when a task gets faster and cheaper to perform, there’s more demand for it.

We’ve written a paper looking at how lawyers should adapt. There’s a lot in there, but one simple point is that AI has made it a lot easier to file lawsuits. And this means more work for lawyers. We might be unhappy about this if we don’t like living in a litigious society, but from the point of view of employment for lawyers, this is great news.

Translation is a more extreme example which kind of blows my mind. AI was almost at human parity nearly a decade ago. Yet the employment of human translators has remained more or less stable, and is projected to remain stable over the next decade. There are many reasons for that, but one reason is that there’s really no ceiling to the amount of things you can translate and the number of different languages you can translate them into.

I’ve argued that so far, our framework is very consistent with the evidence. Now let’s talk about the possibility that everything I’ve said so far will be obviated because some kind of takeoff or singularity will be achieved, and at that point there will be nothing left for us to do.

Many companies have said, including these two, that they are racing towards recursive self-improvement. And they are serious companies — I take them seriously.

Suppose, some time in the next year, recursive self-improvement is achieved. What are the consequences? The slide shows the usual view in the AI community. Note that AGI a famously slippery term. There are two main groups of definitions. One is that AGI will be humanlike in a range of cognitive dimensions, and the other is that it will be capable of performing a range of economically valuable tasks.

My claim is that this view — that treats AGI and ASI as near-automatic consequences of RSI — is a little bit silly. These are four different dimensions of progress. None of these dimensions implies any of the others.

I’ll talk about the details on the next few slides, but one small bit of intuition for now. One of the things that’s associated with superintelligence is that it will cure cancer and other diseas. But we know that the hard part of developing medical treatments is clinical trials that often require thousands of people and 10-15 years. That’s an example of the fact that the bottlenecks to superintelligence are external. It is not something that you can solve in the lab through purely computational processes. So the idea that recursive self-improvement will automatically lead to superintelligence, like many commentators assume — clearly there is something missing in terms of the causal chain.

Early on in the history of AI, these were all distant goals, so it was okay that we didn’t clearly distinguish between different dimensions of progress. But now it’s becoming a real problem — it’s leading to confused discourse.

As an analogy, suppose we are early explorers and we hope to go to Hawaii one day. It’s okay that we have one single term for this group of islands. But as our ship gets closer, we’d better be able to talk about these islands with different words. Otherwise, we’re going to confuse ourselves about where we are and where we’re going.

Helen Toner, among others, has made similar points.

Now let’s discuss each of the four dimensions in detail, starting with Recursive Self-Improvement.

Suppose a company claims they have built RSI — they have built an AI system which built its own successor. What does that actually mean?

On the one hand, maybe it means an LLM spit out a whole bunch of ideas for how to tweak the architecture or the data pipeline or whatever else, and they automatically tested it and kept the improvements that worked. That’s just glorified hyperparameter search. We’ve had systems like AutoML for a long time — I looked it up, and even Schmidhuber has published about this a long time ago. We’re not calling that superintelligence. So that’s one end of the spectrum.

It’s very different from the other extreme, where you can imagine the company actually manages to replace the creativity and intelligence of the human AI researchers — not just one researcher, and not just researchers working at the company, but the entire worldwide community of hundreds of thousands of people whose innovations are all going into improving AI systems. So when people are talking about RSI, it’s not clear which of these they mean.

The reason for my prediction is simple: AI is still in a state where it is much better at verifiable tasks than non-verifiable tasks. Some dimensions of AI performance, like speed and efficiency, are verifiable, and it is possible that they could be greatly improved through near-term RSI.

However, creativity, among other dimensions of intelligence, is the epitome of an unverifiable task.

We don’t even know clearly how to test AI creativity. We don’t understand human creativity well enough from a cognitive science and neuroscience perspective to be able to have clear tests for it.

Let’s go a bit deeper into AI creativity as an illustration of the barriers to humanlike AI.

This quip from podcaster Dwarkesh Patel is a good illustration of how AI creativity lags human creativity. There might be a few counterexamples, especially in narrow domains like Erdos problems. But think about famous “Eureka moments” — finding surprising connections between seemingly unrelated fields or problems — that we associate with great feats of human scientific invention and creativity. LLMs seem to be nowhere close to that.

To understand why, I spent a good amount of time immersed in the cognitive science literature. I won’t go into the details, but seems like there are two literatures looking at this in slightly different ways that haven’t really been talking to each other, for reasons I don’t fully understand. Based on what I’ve been reading, I want to present some hypotheses about AI creativity.

Representation quality is absolutely fundamental to cognition. Think about deep learning — what a big leap that was in the quality of representations behind perception. So I do think AI has more or less caught up in representation quality when it comes to perception, but it has not caught up when it comes to representations that we use as humans for creativity and reasoning.

François Chollet in particular has talked about the fact that human representations that underpin our creativity seem to exhibit a kind of extreme compositionality, where everything is built up from a few “atoms of meaning”. Many seeming limitations of human working memory and information processing actually turn out to be strengths, because they force us to come up with these extremely efficient representations.

Because LLMs are much better than humans in certain dimensions — like memorizing and retrieving stored patterns that might enable creativity — we’re not able to elicit these important real limitations in studies that try to compare human and LLM creativity.

Furthermore, we humans can do something beautiful when we’re creatively thinking about a problem: we improve our representations related to the problem in real time — at inference time, if you will. That enables us to sleep on it, improve our representations, and come back the next day much more efficient at solving that particular problem. I’m sure we’ve all experienced this. But it is something that today’s AI systems are not able to do.

I assumed naively that the continual learning people would be all over this, but when I started looking into that literature, it appears to me that that’s not the case. Continual learning is mostly focused on preventing catastrophic forgetting, as opposed to improving things over time. And furthermore, it is more focused on facts, skills, etc., as opposed to the quality of the underlying representations.

So putting all this together, and considering the fact that creativity is only one of many barriers to humanlike AI, I do think there is still a very long way to go. That said, I want to express some humility here. These are all just hypotheses. I think we need empirical verification.

Speaking of empirical measurement, we have an ongoing project testing AI’s ability to do humanlike AI research.

We give AI agents a budget of a few thousand dollars and a machine learning research problem — a problem that researchers have already worked on and written a paper about, but not yet made public on arXiv. This combines two properties: it gives us a team of human judges who’ve thought deeply about this problem for months and can therefore judge the AI output, but their thinking is not yet online, so it prevents AI cheating or contamination.

Many others have done auto-research experiments, but we’re trying to do some things differently — in particular, picking somewhat open-ended problems, so that we can actually test the ability of AI agents at exercising judgment and creativity. We hope to release detailed findings very soon.

This builds on a foundation that we call open-world evaluation. We’ve completed one open-world evaluation of getting agents to build and upload an app to the Apple App Store — that’s not about recursive self-improvement, but it tests a different kind of thing. We have assembled a great team, with people from many universities, a couple of companies, as well as the UK AI Security Institute.

And by the way, we are hiring a researcher to lead some of our future open-world evaluations. If you’re interested, go check out our website.

Now let’s turn to the third of the four dimensions.

Some people say that AGI is already here. Well, this is one way to interpret that claim, and I happen to agree with it.

This might seem surprising given the skepticism of rapid economic impacts that I expressed in Part 1. But this is actually not only consistent, but in fact a restatement of Part 1. My point there was that the barriers are downstream, and therefore model improvements won’t rapidly change the economy. But by the very same token, because the barriers are downstream, even without model improvements, those barriers are gradually going to get addressed. Yes, it might take a couple of decades, but we will definitely get there.

There are many barriers: reliability (already discussed); integrating AI models into various existing systems; tacit knowledge from domains like medicine or law or various other professions that need to be made available to the models; regulation that often straight up prohibits today’s AI systems from being used in productive ways — in many cases there are good reasons for that regulation, but it will need to be modified in order to be able to enable adoption.

The key point is that these are not things that will be solved in the lab. These are not things that are going to be addressed by the next model release on Tuesday. They will be addressed gradually, through gradual adoption, over a period of decades.

From a geopolitics or strategic perspective, some people advocate a race to AGI, arguing that, similar to a Manhattan Project, the country that gets to some capability milestone first is the one that is going to reap economic rewards. I strongly disagree.

There is no particular capability milestone that will unlock all of this economic potential. The economic potential is already there. It really depends on all these downstream actions that we take. It is not gated by capability.

Okay. Now let me talk about the last of these dimensions, namely superintelligence.

I gave the example of medical trials earlier. Another example: Do you think that we will get superintelligence that can predict the weather precisely a year into the future? We know that that is basically a mathematical impossibility because of the theory of chaos in nonlinear dynamical systems. Our position is that a surprising number of tasks are similar to weather prediction, in that there are inherent limits and we are pretty much already at those limits.

Even for tasks where we’re not at that limit, the idea that there is going to be AI superintelligence that’s going to obviate humans relies on a fundamental misunderstanding of human intelligence. I claim that in the vast majority of tasks, our performance is not limited by our biology — it is rather limited by our learning and tools.

If you imagine someone from the ancient past time traveling to our world, we’re superintelligent compared to that person. And it’s not because our biology is better, but because they don’t have the benefit of all the learning that we’ve been through and all the tools, especially digital tools, that we’re able to use in order to be productive at whatever it is that we do.

And AI is one such tool. So what that means in our framework is that improvements in AI are actually improving human intelligence, not just AI intelligence. So we have a race between the performance of AI-augmented humans on the one hand and the performance of AI systems acting alone on the other hand. I think we can ensure that we are the superintelligences of the future and we win that race, as opposed to allowing AI systems to act alone in ways that would threaten our future and control.

Many people are pessimistic about this. They bring up thought experiments such as AI running and owning companies in the future. Their view is that if the AI system is not aligned, terrible things might happen — the AI could turn into a paperclip maximizer or do other catastrophic stuff. Our perspective is very different. If you’re imagining a future where AI is actually owning and running companies and hiring and firing people, that’s already dystopian. It doesn’t matter if that AI is aligned or not. The consequences for human dignity. democratic governance, etc. are already catastrophic.

So if your view on safety is to treat this as an inevitable future and just to hope for “alignment” to solve that problem — pardon me for being a little bit blunt here — it feels to me that you’re against safety, not for safety. I think it will take a lot of hard work to ensure that we don’t irresponsibly deploy AI systems in this kind of fashion. But I do think we can get there. And that’s the reason I’ve spent a lot of my career advising policymakers. We’re going to need policy and we’re going to need politics, and it’s going to be hard, but let’s not give up that fight before it even starts.

For example, RSI is achievable in the lab, but it won’t immediately put people out of work. On the other hand, economically transformative AI is going to happen, but it’s not because of the next model release — it’s because of things that will gradually happen over the next couple of decades.

Connecting back to the title of this talk: there’s no world in which something that an AI company will decide to do in a lab will put us all out of work. Yes, there are risks to be worried about. Yes, things are going to change. But we have agency over how AI gets deployed, and that process will unfold over decades. Again, this is not guaranteed, but this is the future that I want to work towards, and this is the reason why I’m cautiously optimistic.

Let me take the last fifteen minutes to talk about the flip side. I do think a lot of things are going to change. What are some of them?

Technical skills tend to be verifiable tasks, and AI will continue to get better at them.

Over twenty years ago, there started to be a stark difference in the labor demand for programming jobs versus software engineering jobs. Programming jobs are conceived narrowly around the technical skills of coding and debugging. Software engineering jobs are responsible for all three layers of the decide, execute, deliver sandwich that I talked about — figuring out what even needs to be built, understanding customers, that sort of thing. It requires domain knowledge, judgment, and more.

I predict that this will happen in more and more fields over time.

A recurring pattern I’ve observed — effort shifts from building systems to evaluating systems. As I’ve mentioned, I lead a team working on AI agent evaluation. LLMs and agents are general purpose. So each time capability goes up, it creates demand for evaluation in a legal setting or a journalistic setting or whatever other setting, and that’s not work that is scalable.

Not only is AI agent evaluation resistant to automation — it has become sufficiently specialized that the set of people and teams working on evaluation is starting to diverge from the set of people and teams building and pushing the state of the art in AI agents. This new community is developing a new set of best practices around what it means to rigorously evaluate agents, and we have a forthcoming paper that is going to look at that in some detail.

Here’s an a metaphor to help explain the shift I see in our community. Imagine that in the past most boats were rowboats, and the work of the humans was in physically moving the boat. There was no separate specialized role around steering the boat: when you’re rowing the boat, you’re also figuring out which way to row the boat.

But what happened when the physical work of moving of the ship could be delegated to the engines? The human jobs didn’t go away. In fact, they became much more specialized. Modern ships have very complicated control panels, and they might have dozens of different specialized roles that are focused on where the ship should go and how it should get there.

I would argue that we’re seeing a similar shift in AI/ML. In the past, most of our work was on building — we didn’t need separate roles for evaluation. That has changed now. We’re still in the early stages of this process, but I think over time more and more of the building, because it’s a verifiable task, will be able to be done by AI, whereas it’s the evaluation — figuring out where we should go as a community, figuring out what are the desirable properties in AI systems — that is very resistant to automation. So a greater fraction of the community’s attention will have to focus on evaluation over time, compared to where it is today.

In rowing, physical strength was valorized, and today we might be sad about the fact that sailors don’t need to be strong and it’s all a bunch of nerds. Similarly, today in the AI community, there’s still great value in deep technical understanding of models and systems, and that is considered the coolest thing; the most important thing; that’s the thing that commands a lot of value. But I think in the future that will become less important than exercising judgment and all of these fuzzy things that have a lower status in the AI community today. That’s a mental shift that we’re maybe not quite prepared for. Many of us will be sad about it, and that’s okay.

One practical consequence of this shift: consider a conference like this one. What fraction of the conference should be dedicated to evaluation papers?

I don’t know, but I think maybe a lot more than it is today. Maybe around half — I’m just throwing a number out there — which is orders of magnitude more than it is today. At the very least we need a dedicated track. We don’t have a dedicated track. NeurIPS does have one, and it has been growing in popularity over time, and I think that is a good thing.

And I would go further and argue that thoughtful evaluation of AI systems is a form of alignment. It’s not aligning the AI system itself, but it’s aligning the community as a whole — thinking about where we’re currently going and where we want to go, and trying to align those two to each other. And without enough emphasis on evaluation, my worry is that the community — going back to the ship metaphor — will behave like a rudderless ship: very powerful, but where we don’t collectively have control over what kind of AI we want to develop.

I had a small role in this cool position paper. A one-sentence version of the argument: we need to move beyond benchmarks being the be-all and end-all of what AI research is considered valuable.

What benchmarks give us is efficiency. We don’t have to spend years peer-reviewing papers — if it beats the state of the art, we know it’s probably a good paper. But unfortunately, it has narrowed the collective vision of the community. We’re searching under streetlights — we are searching the kinds of things that are easy to search in a benchmark-driven evaluation regime. We need to move beyond that. That means evaluating how good a paper is will become much more costly. Sadly, that is a cost that we will have to pay. Right now the community is trying to rush in the opposite direction.

There is a big temptation towards automating peer review. And with all due respect, I think this is a trap. If we automate peer review, we essentially give up control over the direction of progress to AI systems themselves. That seems like a fundamental misallocation of effort.

If we automate the mundane parts, human time can be freed up to think more deeply about the parts that require judgment.

Generalizing from AI research to scientific research as a whole, we have an essay that argues that visions of “automating science” rely on a fundamental misunderstanding — as if the point of science is mere problem solving, and that if we can use AI to get directly from the problem to the solution, we will have automated and accelerated science.

In our view, human understanding is not some friction to be automated away. It is rather essential. It is central to the very purposes behind why we do science. And if we lose human understanding, we lose all of these things that flow from it.

So my prediction is that if we are going to have increased use of AI agents in science, there will be new tools and new roles that are specialized towards looking at those AI solutions and backing out the human understanding from those solutions. Because it is essential to preserve human understanding.

Many people in the corporate world are saying that evals are the new IP. Without going into the details, it is very much the same phenomenon playing out once again — effort shifting from purely building to a mix of building and evaluation of systems.

(Note: in this post I explain the idea of cross-functional eval teams to keep companies from fooling themselves.)

If there’s only one thing you take away from this talk, it should probably be this slide. What I’ve shown throughout the talk is that in many different areas, because the verifiable tasks can be handled by AI, some or much of the effort shifts from building to evaluation.

Effort shifts from “rowing the boat” to “steering the ship, navigating the ship, and figuring out where we even want to go”.

I think this is a powerful metaphor that will allow us to predict how human roles will shift over the next decade or two.

In the last few minutes, let me give you some personal reflections on how I’ve been struggling through this challenge — the fact that AI capabilities are rapidly improving — in my own research workflows, as I’m sure many of you are. I think everyone should choose their own path, but hopefully there are some interesting ideas here for you to consider.

By floor I mean what AI can do on its own. By ceiling I mean what AI allows us to do by augmenting our capabilities — the ability to take on new and ambitious projects that were not possible before AI. But the ceiling is not going up automatically. It is only if we work on pushing the ceiling up.

I find that I’m spending something like 10 hours per week just learning and experimenting with new workflows, as well as learning new topics. So one way to think about it is that AI enables big productivity improvements, and I’ve been trying to take that time saved and “reinvest” it into long-term growth and picking up new and complementary skills.

I’ve learned that if I don’t feel exhausted at the end of the day, I’ve done something wrong. I’ve offloaded too much to AI. I’ve sacrificed too much of my long-term growth in the pursuit of short-term productivity.

Growth and productivity are two legs of a three-legged stool that we need to learn how to balance. And the third leg of that stool is staying in control.

Here are two heuristics that I’ve tried to use in order to do that. The first is resisting the black-box temptation. Companies want us to use agents as black boxes — just prompt it and it will go off and do its thing. I think that’s a trap. I think that’s very dangerous, and it will lead us to gradually give up control over time.

Second, and related, is what I call the dependence spiral. It’s very tempting to use AI for tasks that I am myself not yet an expert at, because learning new things is of course very hard. But that leads to my losing whatever little skills I have in that task over. It’s much better in the long run if I first put in the time to master the task myself before I use AI to augment my productivity.

None of this is easy, but if we get this right, the vision is tantalizing and empowering.

Computers have often been called bicycles for the mind. I think AI can be more than that. I think AI can be a crane for the mind, if you will indulge my metaphor, in the sense that it can amplify our potential to previously unimaginable heights. Getting there can seem daunting. It has an incredible learning curve. I feel like I’m on a treadmill all the time, but I’m very excited about it. I think it’s a fun challenge, and I think it’s worth fighting this fight. Compared to five years ago, in a way, I feel ... maybe superintelligent is not the right word, but I feel like I have superpowers, given the extent to which AI allows me to take on new ambitious things that were not possible before, and push myself harder than was possible before.

And I think we can ensure that this remains the case even as AI capabilities advance, for the foreseeable future. It might be that in some distant future this becomes impossible to do, but it is very premature to give up the fight now. I certainly plan to continue this fight, and I hope to work towards this vision of co-superintelligence, and I hope you will join me. That is my closing thought.

Read the whole story
emrox
4 hours ago
reply
Hamburg, Germany
Share this story
Delete

(comic) Premature optimization

1 Share

Read the whole story
emrox
4 hours ago
reply
Hamburg, Germany
Share this story
Delete

A full body MRI earns you a year of smoking

1 Share

full-body-mri-earns-you-a-base-jump.jpg

Alternative titles:

  • … earns you a high-risk pregnancy
  • … earns you an ascent of Matterhorn
  • … earns you 10,000 km on a motorcycle
  • … earns you two base jumps
  • … earns you a day on the frontline in Ukraine

These are all about equivalent to the risk of one year of smoking.


I’m skeptical of submitting asymptomatic people to medical tests. Almost any time I look into the evidentiary power of medical tests, I’m struck by how much work is performed by the base rate. The tests only work because we perform them on people who appear sick, i.e. have a higher probability of actually being sick in the first place. If we would perform them on seemingly-healthy people we would get nonsensical results.1 Note that this is not a criticism of the tests. They are optimised for the thing they need to do.

Thus it was with interest I read Scott Alexander’s breakdown of the benefits and costs of a routine full-body mri as a way of screening for cancer. However, I felt like the conclusions weren’t put into an understandable context. Here’s my attempt at doing so.


First, a quick recap of the main points of the article. I’ll ignore the exorbitant financial costs of us healthcare, and focus on the benefit and cost in terms of health. This is measured in quality-adjusted life years, or qalys. Of the hypothetical thousand people who get scanned, the estimation is that

  • 680 people will be fine, and this costs them only the doctor’s visit, which is around 3 hours, or 0.0003 qalys.
  • 296 people will undergo additional waiting and testing unnecessarily, at a total cost of 0.02 qalys coming from side effects, anxiety, and patient time.
  • 10 people will have unnecessary biopsies on top of the additional waiting and testing above, bringing their total cost to 0.06 qalys.
  • 6 people will detect a real problem, but in a way that doesn’t help them. We’ll count this as no benefit, but also no cost.2 They would probably have gone through this circus eventually anyway.
  • 4 people will benefit from early detection already at the mri stage, and gain an average of 4 qalys from this, and at a cost of 0.007 qalys after patient time and side effects, this is still a gain of 3.99 qalys.
  • 4 people will benefit from early detection only after additional rounds of testing. They still gain the 4 qalys but their cost is ten more hours of patient time and some additional anxiety. It still ends up being a net benefit of 3.99 qalys.

Tallying up the costs and benefits into an expected value, we get a net benefit of 0.025 qalys per person, after accounting for medical time.3 This is the figure Scott Alexander reports as “25” in sum across all 1000 people. I just didn’t find its very clear so I had to replicate it to make sense of it. This doesn’t tell me a whole lot, because my intuition for qalys is weak. How strongly should I prefer an intervention with a net benefit of 0.025 qalys over other things I might do with my time? No idea!

However! When marketing the effect of global health interventions, a count of 27 qalys is typically considered “a life saved”. A life also happens to be a million micromorts, and I have a much better intuition for micromorts! When we run that maths in reverse, we get a very cheeky exchange rate between qalys and micromorts:

One qaly is 37,000 micromorts.4 The equivalence between 27 qalys and a life saved is based on global demographics. For people in developed countries, where the life expectation is longer, a qaly probably corresponds to a lower number of micromorts – probably around 30,000 or so.

Thus, an intervention that has an expected benefit of 0.025 qalys – like a routine full-body mri – corresponds to an intervention that has a benefit of 926 micromorts. The alternative titles indicate activities that carry roughly a risk of 1000 micromorts:

  • a year of smoking,
  • a high-risk pregnancy,
  • an ascent of Matterhorn,
  • riding 10,000 km on a motorcycle,
  • two base jumps, and
  • one day on the frontline in Ukraine.

So the same effort you would expend to get out of those activities on account of their risk, the same effort you should be willing to expend to get a full-body mri.

Note the on account of their risk phrasing. I have no interest in doing any number of base jumps and will work hard to get out of doing them, but that’s because I wouldn’t enjoy the activity, not because I’m worried about the risk. If I was put in a situation where it seemed like I had to perform two base jumps to reunite with my family, I would spend some effort on finding alternative ways of getting there, but perhaps not all that much before I decide to just eat the base jumps and getting it over with.

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

What's new in ECMAScript 2026

1 Share

The reason I write more Go than JavaScript nowadays is not because there is anything wrong with the language, but because I’m tired of the ecosystem. The language on its own is really good, it is the first programming language that I became productive with and I’m still motivated to keep up with the new additions to its specification. Every year after the final language specification approval, I publish about the latest additions to the language. If you’re curious how the language has changed over the last decade, here is a full list.

On 30 June 2026, Ecma International approved the ECMAScript® 2026 language specification, so here is a summary of the new additions.

Array.fromAsync#

To convert a synchronous iterator into an array, we can either use a for loop, or Array.from(). This has been available in the language since 2015.

function* gen(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const arrOne = [];
for (const n of gen(2024, 2026)) {
  arrOne.push(`ECMAScript ${n}`);
}
console.log("arrOne", arrOne);
// arrOne [ 'ECMAScript 2024', 'ECMAScript 2025', 'ECMAScript 2026' ]

const arrTwo = Array.from(gen(2024, 2026), (n) => `ECMAScript ${n}`);
console.log("arrTwo", arrTwo);
// arrTwo [ 'ECMAScript 2024', 'ECMAScript 2025', 'ECMAScript 2026' ]

For asynchronous iterators for the last few years, we only had a for await loop and no static method helper on the Array prototype.

async function* gen(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const arr = [];
for await (const n of gen(2024, 2026)) {
  arr.push(`ECMAScript ${n}`);
}
console.log("arr", arr);
// arr [ 'ECMAScript 2024', 'ECMAScript 2025', 'ECMAScript 2026' ]

This year, the Array.fromAsync is joining the specification to fill this gap. It produces an array from an async iterator, or a sync generator that yields promises. Thanks to J.S. Choi for the Array.fromAsync for JavaScript proposal.

async function* asyncGen(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const arrOne = [];
for await (const n of asyncGen(2024, 2026)) {
  arrOne.push(`ECMAScript ${n}`);
}
console.log("arrOne", arrOne);
// arrOne [ 'ECMAScript 2024', 'ECMAScript 2025', 'ECMAScript 2026' ]

const arrTwo = await Array.fromAsync(
  asyncGen(2024, 2026),
  (n) => `ECMAScript ${n}`,
);
console.log("arrTwo", arrTwo);
// arrTwo [ 'ECMAScript 2024', 'ECMAScript 2025', 'ECMAScript 2026' ]
function* gen(start, end) {
  for (let i = start; i <= end; i++) {
    yield Promise.resolve(i);
  }
}

const arr = await Array.fromAsync(gen(2024, 2026), (n) => `ECMAScript ${n}`);
console.log("arr", arr);
// arr [ 'ECMAScript 2024', 'ECMAScript 2025', 'ECMAScript 2026' ]

Error.isError#

Thanks to Jordan Harband for the Error.isError() proposal. A much safer alternative to the instanceof Error.

try {
  throw new Error("This is an error");
} catch (error) {
  console.log(Error.isError(error));
  // true
}
try {
  throw "This is not an error";
} catch (error) {
  console.log(Error.isError(error));
  // false
}

Math.sumPrecise#

Adding a list of numbers together is such a frequent use case for the Array.reduce() function. Thanks to Kevin Gibbons, who submitted a proposal for the Math.sumPrecise function, we no longer need reducers and, as a bonus, we’re getting better precision for floating-point numbers.

const values = [1e20, 0.1, -1e20];

const sumOne = values.reduce((a, b) => a + b, 0);
console.log("sumOne", sumOne);
// sumOne 0

const sumTwo = Math.sumPrecise(values);
console.log("sumTwo", sumTwo);
// sumTwo 0.1

Uint8Array to/from Base64 and hex#

A built-in mechanism for converting Uint8Array to base64 and hex values and back. Another addition to the language that will remove an additional dependency.

const value = new Uint8Array([69, 83, 50, 48, 50, 54]);

const valueBase64 = value.toBase64();
const valueHex = value.toHex();

console.log({ value });
console.log({ valueBase64 });
// { value: Uint8Array(6) [ 69, 83, 50, 48, 50, 54 ] }
// { valueBase64: 'RVMyMDI2' }

const valueBase64Decoded = Uint8Array.fromBase64(valueBase64);
const valueHexDecoded = Uint8Array.fromHex(valueHex);

console.log({ valueBase64Decoded });
console.log({ valueHexDecoded });
// { valueBase64Decoded: Uint8Array(6) [ 69, 83, 50, 48, 50, 54 ] }
// { valueHexDecoded: Uint8Array(6) [ 69, 83, 50, 48, 50, 54 ] }

// 😜
console.log(new TextDecoder().decode(valueBase64Decoded));
// ES2026

Iterator Sequencing#

Before the ES2026, combining iterators meant creating a generator that sequentially yields iterators. Look at this example.

const iOne = Iterator.from([2022, 2023]);
const iTwo = Iterator.from([2025, 2026]);

function* combine(...iterators) {
  for (const source of iterators) {
    yield* source;
  }
}

const combinedIterator = combine(iOne, iTwo);
console.log("combinedIterator", Array.from(combinedIterator));
// combinedIterator [ 2022, 2023, 2025, 2026 ]

Thanks to Michael Ficarra, we no longer need to do that dance because the proposal for the Iterator Sequencing landed. As a nice bonus, it also offers a convenient way of slotting values in (look at the 2024 in the example below).

const iOne = Iterator.from([2022, 2023]);
const iTwo = Iterator.from([2025, 2026]);

const combinedIterator = Iterator.concat(iOne, [2024], iTwo);
console.log("combinedIterator", Array.from(combinedIterator));
// combinedIterator [ 2022, 2023, 2024, 2025, 2026 ]

JSON.parse source text access#

JSON marshalling/unmarshalling is lossy in JavaScript. Look at these two examples where deserialization produces an incorrect value and serialization that results in a TypeError for a perfectly valid BigInt.

console.log(JSON.parse("999999999999999999"));
// 1000000000000000000
console.log(JSON.stringify(9999999999999999n));
// TypeError: Do not know how to serialize a BigInt

The JSON.parse source text access proposal by Richard Gibson comes with a solution for both of these issues. The reviver callback of the JSON.parse, in addition to the good old key and value, now also exposes a third argument that gives us access to the original source value. Similarly, on the JSON.stringify replacer function, we can pass the value through the new JSON.rawJSON().

console.log(
  JSON.parse("999999999999999999", (key, value, { source }) => BigInt(source)),
);
// 999999999999999999n

console.log(
  JSON.stringify(9999999999999999n, (key, value) => JSON.rawJSON(value)),
);
// 9999999999999999

Upsert#

This one is a pure convenience and will reduce tons of boilerplate code. No need to explicitly check for the existence of the key on the Map/WeakMap before inserting, because the upserting is now possible using the getOrInsert method. The Upsert proposal became part of the language because of the hard work by Daniel Minor, Lauritz Thoresen Angeltveit, Jonas Haukenes, Sune Lianes, Vetle Larsen and Mathias Hop Ness. Thank you!

const settings = new Map();
settings.set("language", "en");

// no need for this dance anymore 🎉
// if (!settings.has("theme")) {
//   settings.set("theme", "dark");
// }
// if (!settings.has("language")) {
//   settings.set("language", "pl");
// }

console.log(settings.getOrInsert("theme", "dark"));
// dark
console.log(settings.getOrInsert("language", "pl"));
// en
Read the whole story
emrox
1 day ago
reply
Hamburg, Germany
Share this story
Delete

Different hydration and rendering strategies (Next.js)

1 Share

Dont worry, this article is not about the controversial Hydration breaks they are having during the World Cup matches.

We want to discuss the hydration process in Server-Side Rendered applications, how other frameworks handle it, and similar rendering strategies that we apply to make our apps faster.

First, open a server-rendered page on a slow phone.

The content paints almost instantly and looks finished. But tapping a button does nothing for 1 or 2 seconds.

During that time, the page quietly downloads and runs JavaScript to rebuild everything you’re already looking at.

That gap between a page that “looks ready” (HTML is shown) and one that “actually works” (interactive via JavaScript) is called hydration.

Every recent rendering strategy aims to shrink that gap.

Before reducing hydration, let’s look at the alternatives.

For many websites, the best approach is to render HTML once, ahead of time, rather than on every request.

If the page is the same for everyone—a blog post, a docs page, a marketing landing page, you are regenerating identical HTML thousands of times.

Static Site Generation (SSG) renders each page once at build time, writes the result to an HTML file, and serves that file from a CDN.

The server does nothing per request except hand over a file.

The obvious limit is freshness.

If the HTML was built an hour ago, it’s an hour stale. For a blog that’s fine; for a product page with live pricing, it isn’t.

And the build itself becomes the bottleneck at scale: fifty blog posts can be built in seconds, but a hundred thousand product pages can take hours, and every content change means a rebuild.

That freshness problem is what Incremental Static Regeneration (ISR) solves.

You serve the static page as before, but you tell the framework to regenerate it in the background after a set interval, or on demand when the content actually changes.

The first visitor after the interval triggers a quiet rebuild; everyone keeps seeing the last good version until the new one is ready.

You get static delivery speed with content that’s never more than a minute or two stale.

One more variation you’ll see is edge rendering.

This isn’t a different hydration model; it’s SSR or ISR physically moved closer to the user, running on CDN edge servers scattered around the world rather than a single origin server.

You do this to reduce latency: your dynamic HTML is generated a few milliseconds from the visitor rather than a continent away, with the trade-off that edge runtimes are more constrained in what they can do (limited Node APIs, tighter time and memory budgets).

The simplest approach is to avoid rendering initial HTML on the server.

The server sends an almost empty HTML shell. This is just the basic structure of an HTML page, with a reserved section for your app, such as a '

' element, and a script tag that points to your JavaScript code.

The browser then downloads the bundle, which combines all your app’s code into a single file, runs your app, fetches any required data, and builds the entire user interface (UI) in the browser.

There’s no hydration here because there’s nothing to hydrate: the DOM (Document Object Model—the browser’s representation of a web page’s structure and content) was never server-rendered; it was constructed on the client from nothing.

React just builds and attaches in one pass.

We call these apps Single Page Applications or SPA for short.

The main benefit of these apps is simplicity. You only think about the browser.

The downside is speed.

Picture a first-time visitor on a mid-range Android phone, using hotel Wi-Fi to tap your link. They get a blank white screen.

That screen stays while the bundle downloads, then parses, and then executes.

Next, it waits as it fetches the page’s data. Only after all that does anything work.

On your laptop with strong WIFI, you might not notice.

On that phone, it’s long enough for some visitors to close the tab before your app draws a single pixel.

Search engines hit the same wall. The Google bot crawler, requests your page and receives an empty

, which may also have significant SEO implications.

That’s why you want content in the initial HTML, and for that, we need the server.

Server-side rendering

We render HTML on the server, send it to the browser, and have the browser hydrate it into a React app.

The user gets real content instantly, solving both CSR issues. The page isn’t blank while JavaScript loads, and the crawler sees content.

import { renderToString } from 'react-dom/server';
import App from './App';

const html = renderToString(<App />);
// send `html` wrapped in your document shell

On the client:

import { hydrateRoot } from 'react-dom/client';

hydrateRoot(document.getElementById('root'), <App />);

This is ideal for content that must rank and be interactive, such as news sites, blogs, marketing pages with signup forms, or product pages that need strong SEO content.

The downside: we repeat work.

The server renders HTML; the client downloads React, rerenders, and compares the new HTML to the existing HTML. Until this second pass finishes, nothing on the page is interactive.

Traditional hydration runs top to bottom through the tree. If a heavy Comments section is near the top and the LikeButton below, hydration does comments first.

The like button stays dead until hydration is done.

There is also a big problem: hydration mismatches.

If server HTML and client HTML differ in any way, React detects it.

In the worst case, it discards server HTML and re-renders on the client, recreating the blank-screen problem that SSR tries to solve, rendering all the benefits of SSR useless.

Common causes are unassuming code:

// Renders a different value on the server than in the browser:
<span>{new Date().toLocaleTimeString()}</span>

// Reads a browser-only API that doesn't exist on the server:
<div>{window.innerWidth > 768 ? 'desktop' : 'mobile'}</div>

Both look harmless but produce different server and client outputs, triggering hydration warnings.

If the difference is intentional, like a timestamp, React offers an escape: add suppressHydrationWarning, and React won’t warn for that node.

Streaming SSR: send the page in pieces

The first improvement keeps hydration but stops making the user wait for the slowest part of the page before anything shows up.

Instead of rendering the whole tree to a string and sending it once it’s complete, the server streams HTML in chunks as each part becomes ready.

The tool for this is Suspense, a wrapper you put around a slow section that tells React, “show this fallback until everything inside is ready.”

import { Suspense } from 'react';

function ProductPage() {
  return (
    <div>
      <ProductDetails />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>
    </div>
  );
}

The ProductDetails component renders immediately.

The Reviews component, which might be waiting on a slow query, shows a skeleton, and the real reviews stream in when the data is ready.

The user reads the product while the reviews are still loading.

React no longer has to hydrate top-to-bottom. Each Suspense boundary becomes an independent unit that can hydrate on its own, and React prioritizes based on the user’s current activity.

Click a button in a section that hasn’t hydrated yet, and React rushes hydration of that boundary so the handler can run, ahead of the boundaries you’re not touching.

The main benefit of this method is that perceived load time no longer ties to your slowest component.

Go back to that product page. Without streaming, a three-second recommendations query means three seconds before anyone sees the product.

But it’s not perfect: Streaming changes the order and timing of the work, not the amount.

Every interactive component on that page still gets downloaded and re-executed on the client; you’ve made the page feel faster without shipping a single byte less JavaScript.

It also hands you a new way to make things worse if you wrap too much of the page in one Suspense, and a single slow child holds that whole region back:

// One boundary around everything: the fast summary now waits
// on the slow chart, because they share a fallback.
<Suspense fallback={<PageSkeleton />}>
  <Summary />   {/* fast */}
  <SlowChart /> {/* slow */}
</Suspense>

// Separate boundaries: the summary streams in immediately,
//The chart arrives on its own schedule.
<Summary />
<Suspense fallback={<ChartSkeleton />}>
  <SlowChart />
</Suspense>

There’s also another method you’ll see in several frameworks: progressive hydration.

Rather than hydrate every boundary as soon as its code arrives, you defer a component until there’s a reason to wake it up, when it scrolls into view, or when the user first interacts with it.

A footer newsletter form three screens down doesn’t need to hydrate during initial load; it can wait until you scroll near it.

This still hydrates everything eventually, like streaming, but it spreads the work across time and skips parts the user never reaches.

Islands: hydrate only the interactive parts

Most of a typical web page isn’t interactive at all.

A blog post is text, headings, and images, with maybe a comment widget and a newsletter form.

A docs page is almost entirely static with a search box. Why ship and hydrate a JavaScript runtime for the 95% that’s just sitting there as content?

Islands do this in reverse to SSR: The page is static HTML, and you explicitly mark the parts that need to come alive.

Each interactive region is an “island” in a sea of static markup, and each hydrates independently, loading only its own JavaScript.

Astro is the framework that popularized this, and it makes the model concrete with directives.

By default, a component renders to static HTML and ships zero JavaScript. You opt into interactivity per-component:

---
import Header from '../components/Header.astro';
import Newsletter from '../components/Newsletter.tsx';
import Comments from '../components/Comments.tsx';
---

<Header />
<article>
  <h1>My post</h1>
  <p>This is just text. It ships as HTML, no JavaScript.</p>
</article>

<Newsletter client:visible />
<Comments client:visible />

That client:visible is a loading contract, and it’s the progressive hydration idea from earlier.

It tells Astro to render the component to static HTML first, then hydrate it only when it scrolls into view.

The directives cover the common cases: client:load hydrates immediately for above-the-fold interactivity, client:idle waits until the browser is idle, client:visible waits until the component enters the viewport, and client:only skips server rendering entirely for components that depend on browser-only APIs.

Islands take progressive hydration and add the crucial second half: the parts you never mark stay static forever and ship no JavaScript at all.

If the user never scrolls to the comments, the comment widget’s JavaScript never loads.

This fits content-heavy sites almost perfectly.

Blogs, documentation, marketing pages, news, and e-commerce category pages. Anywhere the page is mostly content with islands of interactivity rather than the other way around.

The framework built around this, Astro, ranked highest in the meta-framework satisfaction category of the State of JS 2025 survey, and Cloudflare acquired it in January 2026. (And if you haven’t read my previous articles, this blog is also using Astro, checkout my previous article where I upgraded to Astro 6 and took advantage of all the cool benefits)

The practical result is that you land a Lighthouse score in the 90s without doing anything clever, because there’s barely any main-thread work for the browser to do.

And since each island is self-contained, you’re not even locked to one framework; you can drop a React island next to a Svelte one on the same page.

There’s a server-side version of the same idea. Astro’s server:defer turns a component into a server island: the static shell ships immediately and is aggressively cached, while a personalized piece, such as a signed-in user’s avatar or cart, renders per-request on the server and slots in without holding up the rest of the page.

You get to cache the 95%, that’s the same for everyone, and still serve the 5% that isn’t.

But islands assume a mostly static page with sprinkles of interactivity, and they’re wonderful until that assumption breaks.

Imagine trying to build Figma, or a trading terminal, or any app where nearly everything on screen is interactive, and pieces share state across the whole layout.

You’d be marking the entire page as one giant island, threading shared state between islands that were designed to be isolated, and fighting the architecture the whole way.

At that point, you’ve reinvented SSR with extra steps.

So islands win when interactivity is the exception. For an app where interactivity is the rule, you need a different weapon.

React Server Components: don’t send the component to the client

A Server Component runs only on the server, which means its code is never sent to the browser.

It can hit the database directly, read the filesystem, use secrets, and what it ships to the client isn’t JavaScript; it’s a serialized description of the UI it produced, in a format React calls Flight.

There are now two streams in play: the HTML stream from the streaming section, which is the markup the browser paints, and this Flight stream, which is a serialized component tree the client reconstructs.

RSC uses both. There’s nothing to hydrate for the server parts because there’s no component code on the client to re-execute.

Components are server-only and weightless on the client. Components marked "use client" ship JavaScript and hydrate.

// Server Component, runs on the server, ships no JS
async function ProductPage({ id }) {
  const product = await db.product.findUnique({ where: { id } });

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCartButton productId={product.id} />
    </div>
  );
}
'use client';

// Client Component, this is the part that ships JS and hydrates
function AddToCartButton({ productId }) {
  const [adding, setAdding] = useState(false);
  // ...
}

The product name and description are server-rendered and never become JavaScript on the client.

Only AddToCartButton is visible in the browser.

This is the model Next.js is built around now, and it supports full applications that also include a lot of server-generated, non-interactive content.

The main benefit is that you stop shipping code that the browser will never use.

In that example, your database call and the markup it produces stay on the server; the only JavaScript that reaches the user is AddToCartButton.

You also get to query the database right inside the component, no /api/products/:id route to build, no fetch to wire up, no loading state to manage by hand.

Layer streaming and selective hydration on top, which RSC does for free, and you get a fast first paint, a small bundle, and direct data access in one model.

React 19.2 sharpened the tools in this area. Partial Pre-Rendering lets you render a static shell at build time, cache it at the CDN edge, and stream the dynamic holes per-request, so a single page can be mostly statically cached with a personalized slot or two.

Suspense reveals are batched to match client behavior, and hydration mismatch errors finally name the component that caused them instead of leaving you a cryptic warning.

Although the line between server and client isn’t free, and one "use client" in the wrong place quietly drags a whole subtree into the browser bundle.

Like this:

'use client';

import { HeavyChart } from './HeavyChart'; // also becomes client code

You added "use client" for a single button, and because a client file pulls its imports into the client graph with it, a heavy chart you meant to keep on the server is now in your bundle. “use client creep” is a real failure mode in production codebases.

On top of that, RSC ties you to a framework and a server runtime that uses them; it isn’t something you sprinkle onto a plain SPA you already have.

Also, moving rendering to the server increases your attack surface there, too.

Server Components execute on the server, serialize a payload, and the client deserializes it; each of those steps now runs in a privileged place with access to your database and secrets.

And in late 2025, a serious RSC deserialization vulnerability (CVE-2025-55182) was found being actively exploited and had to be patched across several React and Next.js versions.

Doing this on the server could have a lot of security holes you need to keep an eye on.

TanStack Start: the same primitive, ownership flipped

The Next.js model is server-first.

TanStack Start inverts that. The client stays in charge, and RSCs are demoted from a paradigm to a data type.

The idea is that an RSC payload is just a stream. Specifically, a React Flight stream is the serialized description of UI that the server produced.

TanStack Start treats it as exactly that: bytes you fetch over HTTP, on the client’s terms, whenever you want them, rather than a server-owned tree the framework hands you by default.

You create the server-rendered UI in a server function and load it via a route loader.

Nothing gets marked "use client" to opt out of the server; you opt in to the server only where you want it.

import { createServerFn } from '@tanstack/react-start';
import { renderServerComponent } from '@tanstack/react-start/rsc';

function Greeting() {
  return <h1>Hello from the server</h1>;
}

const getGreeting = createServerFn().handler(async () => {
  return { Greeting: await renderServerComponent(<Greeting />) };
});

Because the payload is just data, it drops into the caching tools you already use. There’s no special “RSC mode.”

Wrap the server function in a TanStack Query call, and the RSC payload gets cache keys, staleTime, and background refetching like any other query.

For static content, set staleTime: Infinity and you’re done.

function Greeting() {
  const query = useQuery({
    queryKey: ['greeting'],
    queryFn: async () => createFromReadableStream(await getGreeting()),
  });

  return <>{query.data}</>;
}

It’s a different answer to the exact same question RSC asks: how do you avoid shipping component code that didn’t need the browser?

Next.js answers at the framework level, server-first, all-in. TanStack Start answers at the data level, is client-first, and is opt-in.

This fits a situation the server-first model handles badly: an existing client-side app, an admin portal or a SaaS dashboard, that wants to move some heavy rendering to the server without rewriting its architecture around a new paradigm.

Markdown parsing, syntax highlighting, a search index, anything heavy and static you’d rather not ship.

You sprinkle in RSCs where they pay off and leave the rest of the SPA alone.

When TanStack migrated the content-heavy parts of their own docs site to this, blog, and docs pages, each dropped around 153 KB gzipped from the client JS graph, and Total Blocking Time fell from about 1,200ms to 260ms.

You don’t have to rewrite your app to try RSCs, but you have to do a lot of the wiring up yourself.

The server-first model’s all-in nature is also what makes streaming, boundaries, and colocated server work feel built in; when you opt into each piece by hand, you own the composition that Next.js provides out of the box.

And TanStack Start is younger, with its RSC support still stabilizing toward 1.0, so for a large production app, you’re taking on a less settled bet than Next.js in exchange for that flexibility.

RSC shrinks the hydration surface to the interactive leaves, no matter how you slice ownership.

But those leaves still hydrate. They still download, re-execute, and rebuild their slice of the tree on the client.

The remaining question is whether even that is necessary, and there are two different answers.

Fine-grained reactivity: hydrate once, then never re-render

React is coarse-grained. When the state changes, the component that owns it reruns, and React walks down from there, diffing the virtual DOM to figure out what actually changed in the real one.

Hydration is expensive partly because it’s this same re-render machinery running for the first time across the whole tree.

SolidJS and Svelte do things differently.

There’s no virtual DOM and no re-rendering. A component runs once to set up a reactive graph, wiring each piece of state directly to the specific DOM node it controls.

When a state changes, the framework updates that one text node or attribute.

import { createSignal } from 'solid-js';

function Counter() {
  const [count, setCount] = createSignal(0);
  // This function body runs ONCE. The signal is wired.
  // straight to the text node below.
  return <button onClick={() => setCount(count() + 1)}>Clicked {count()} times</button>;
}

That Counter function executes only once, during setup. Clicking the button updates the count signal, which updates exactly one text node.

The component never runs again.

Because there’s no re-render machinery, hydration is dramatically cheaper. Solid still incurs a setup cost to wire the reactive graph to the server-rendered DOM, but it’s not re-executing and diffing the whole component tree, so the work is a fraction of what React does during hydration.

Svelte 5 sits in the same territory: its runes system ($state, $derived) moved Svelte to a signal-based, fine-grained reactivity model, close to Solid’s, and State of JS 2025 credited exactly that change as the year’s standout in developer experience.

The newest face in this family is Ripple, an experiment from Dominic Gannaway (who worked on React Hooks at Meta and was on Svelte 5’s core team).

It’s a TypeScript-first compiled language with the same no-virtual-DOM, surgical-update model, reactivity built on a track() primitive.

These frameworks fit performance-critical interactive UIs where things update constantly.

Real-time dashboards with hundreds of live data points, trading interfaces, data grids, and animation-heavy apps targeting a steady 60fps.

Anywhere React’s re-render-and-diff cost compounds across every update on a busy screen.

Of course, React’s download numbers dwarf all of these new frameworks, and that gap is libraries, AI advice and help, and the odds that your next hire already knows the framework.

The meta-frameworks are less settled, too: SvelteKit is solidly production-ready, while SolidStart hit 1.0 but is still filling in deployment adapters and documented patterns, so you’ll more often be the first person to hit a given edge case.

And the mental model differs from React:

// React: this function re-runs on every render, so it's expensive`
// is recomputed each time.
function Row({ value }) {
  const expensive = computeStuff(value);
  return <td>{expensive}</td>;
}

// Solid: this function runs ONCE. If you write it the React way,
// `expensive` is computed a single time and then never updates
// when `value` changes. You have to reach for a derived signal instead.

The code looks like React. It does not behave like React.

Fine-grained reactivity makes hydration cheap. But it still hydrates; there’s still a setup pass that runs on the client to connect the graph to the DOM before anything is interactive.

The last strategy asks whether you can skip even that.

Resumability: don’t hydrate at all

The final move is the strange one, and it needed a new word because it isn’t a faster hydration. It’s the absence of hydration.

Every strategy so far, even the leanest island, shares one assumption: the client has to execute some JavaScript to make the server-rendered HTML interactive.

Re-run the components, or at least run a setup pass to wire up the reactive graph.

Qwik’s argument is that throwing the work away is the actual mistake. Instead of serializing only the HTML and reconstructing everything else on the client, serialize the entire framework’s execution state, component boundaries, event listener locations, and the reactivity graph directly into the HTML.

The client doesn’t rebuild anything. It picks up exactly where the server paused.

The mechanism is visible in the markup. Event handlers aren’t attached during a startup pass; their locations are serialized as attributes pointing at lazy-loadable chunks:

<button on:click="./chunk-abc.js#handler">Add to cart</button>

The only thing that runs on boot is a tiny script called the Qwikloader, a fraction of a kilobyte, which registers one global event listener and does nothing else until you interact.

When the user clicks that button, and only then, the Qwikloader resolves the attribute, downloads the specific handler chunk, rehydrates just the state that handler needs, and runs it.

The code for a given interaction loads when the interaction occurs, not before.

But serializing execution state into the HTML imposes limits on what can be serialized, and those limits constrain how you can write components; you can’t just stuff anything into a closure and expect it to resume.

Loading code per interaction also means more small requests rather than one big upfront bundle, which speculative prefetching and HTTP/2 are designed to hide, but it’s a different performance profile you’ll need to reason about and tune rather than ignore.

And Qwik is the one framework built entirely around this idea, making it the newest and least battle-tested option in this article, with the smallest ecosystem to lean on.

There’s also a case where the payoff simply isn’t there.

If you’re building a video editor or a live dashboard, the user interacts with almost everything almost immediately, so you end up loading most of the code in the first few seconds anyway.

Resumability’s advantage is deferring code the user might never reach; when they reach all of it, you’ve taken on the complexity and gotten little of the benefit.

Next.js 16.3: closing the last gap with instant navigations

Everything so far has been about the first load: how fast the page paints, how much it costs to make it interactive.

But there’s a second moment that decides whether an app feels like an app, and it’s the one server-driven models have always been worst at: what happens when you click a link to the next page.

In a classic server-driven app, that click means a network round-trip.

You click, nothing happens, the server responds, and the next page appears.

For a blog that’s acceptable. For anything that feels like software, that pause is exactly what makes people say server apps feel “like a website” rather than an app.

A client-driven SPA hides the pause: you click, you instantly see a shell of the next page with data still loading, then the rest fills in.

That instant shell is most of why people reach for SPAs even when the server model would serve them better everywhere else.

Next.js 16.3 aims to give the server model the same instant-shell feel, and the way it gets there ties together two things already in this article: Partial Pre-Rendering and the Flight payload that RSC streams to the client.

The whole feature is currently gated behind one flag:

// next.config.ts
const nextConfig = {
  cacheComponents: true,
};

Turning on Cache Components changes the rendering model.

Every route is dynamic by default, with no implicit caching, and PPR becomes the default rather than an experimental per-route opt-in.

From there, whenever a route `awaits ’ data on the server, you’re making one of three choices about that piece of the page.

You can stream it by wrapping the slow part in <Suspense>, and the user instantly sees the static shell with a loading state where that part will go.

You can cache it by marking the work with use cache, and the user instantly sees a previously cached version of that UI, reused across requests.

async function ProductList() {
  'use cache';
  cacheLife('hours');
  const products = await getProducts();
  return <ProductGrid products={products} />;
}

Either of those makes the navigation feel instant, because nothing the user is waiting on blocks the shell.

The third choice is to block on purpose. Some routes shouldn’t show a loading shell, a blog post that should arrive whole rather than skeleton-first, and you opt that route out explicitly:

// page.tsx
export const instant = false;

Next.js 16.3 also brings a new trick: rather than prefetching a page per link, it prefetches one reusable shell per route and caches it on the client for the session.

Twenty links to /chat/[id] now prefetch a single /chat/[id] shell, the same way a SPA ships one piece of per-route code and reuses it for every item.

You can enable it alongside Cache Components:

// next.config.ts
const nextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

That shell is the static, cacheable part of the route, the part PPR already knows how to pre-render.

So the two flags are really one idea seen from two ends: PPR decides what part of a route is a static shell, and Partial Prefetching ships exactly that shell to the client ahead of the click.

Because the shell serves as the baseline, prefetching is no longer an all-or-nothing proposition.

If you want a particular link to arrive with more than the bare shell, a chat header that should pop in immediately, you opt that link into deeper prefetching, and Next.js renders down to whatever is synchronous or marked 'use cache':

<Link href={`/chat/${id}`} prefetch={true}>
  {title}
</Link>

This fits exactly the kind of app the server model used to feel wrong for: dashboards, chat apps, anything with a dense sidebar of links where every click used to mean a visible pause.

Vercel has been running it on v0, where rich client features made navigation feel sluggish, and used the new dev-time insights to hunt down the routes that weren’t navigating instantly.

You get the SPA’s instant-click feel without giving up the server-centric model, the small client bundle, the direct data access, the crawlable first load, all the things the RSC section was about.

The problem is that “instant” stops being free and becomes something you maintain.

Every dynamic piece of every route is now a Stream/Cache/Block decision you have to make and keep correct as the app changes; a refactor that moves a cookies() read out of its <Suspense> boundary quietly turns an instant route into a blocking one.

Next.js leans on tooling to hold the line here, the dev-time error, a Navigation Inspector that pauses navigations at the shell so you can see what’s prefetched, and an instant() Playwright helper that asserts what must be visible before the network responds:

import { instant } from '@next/playwright';

test('next page shell is instant', async ({ page }) => {
  await page.goto('/products/shoes');
  await instant(page, async () => {
    await page.click('a[href="/products/hats"]');
    await expect(page.locator('h1')).toContainText('Baseball Cap');
  });
});

It’s also preview-only and flag-gated as of mid-2026, with both cacheComponents and partialPrefetching planned to become defaults in a future major version.

How to actually choose

The only thing that matters is how much client-side work happens between “HTML painted” and “page interactive.”

For most people most of the time, the honest answer is boring.

If you’re building a content site, reach for static generation and islands; Astro will give you near-perfect performance with almost no effort.

If you’re building an app, React with Server Components on a framework like Next.js will carry you a very long way, and the streaming and selective-hydration machinery comes along for free.

The rest are tools you reach for when a specific problem actually calls for them.

Fine-grained reactivity when updating performance on a busy screen is the cause of the issue.

Resumability when you’re large enough that hydration costs have become the bottleneck, and the first interaction must be instant.

CSR when the page is behind a login, and nobody’s first paint matters.

Nobody’s site ever got slow because they server-rendered some HTML and hydrated it.

It’s always the other direction: a content blog shipping a full app runtime, an app fighting hydration cost it never measured, a marketing page blank for two seconds while a bundle loads.

Match the strategy to how much of your page is actually interactive, and reach for something more exotic only when you can name the problem it solves.

References

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

Announcing TypeScript 7.0

2 Shares

Today we are proud to announce the availability of TypeScript 7, a 10x faster native port of TypeScript!

Since its early days, TypeScript has promised to deliver on JavaScript that scales. By bringing strong type-checking and rich tooling to the world of JavaScript, TypeScript made it possible to build non-trivial high-quality apps across platforms.

Last year, our team unveiled TypeScript’s next step in scaling: making every part of the toolset an order of magnitude faster. The mission was a native port of TypeScript built in Go that could make the most of modern hardware. This port was done as faithfully as possible, writing new code while maintaining the structure and logic of the original codebase to keep results consistent and compatible between the two compilers. The key difference is that with this new codebase, TypeScript 7 brings native code speed, shared memory multithreading, and a number of new optimizations that typically yield speedups between 8x and 12x on full builds.

Just as with any other release, TypeScript 7 is available via npm:

npm install -D typescript

That will get you the new tsc executable in your workspace (which you can run via npx tsc). Of course, a big part of the TypeScript experience is also about editor support. Your favorite code editor should easily support TypeScript 7 with its new support for the language server protocol (LSP), and its new speed and multithreading improvements. Whether you’re using something like VS Code, Visual Studio, WebStorm, or any other modern editor, TypeScript 7 should work great. Just check your editor’s documentation – for example, VS Code has a dedicated extension for TypeScript 7 that you can use today, and Visual Studio will automatically enable TypeScript 7 based on your workspace.

What Does A Faster TypeScript Mean?

A faster TypeScript sounds great on paper, but what does it mean in practice? Maybe it helps to think about where TypeScript comes up at every stage of development.

A typical day of development might involve opening your editor, opening a TypeScript file, and running an operation like find-all-references across your projects. Then as you’d start to make edits, maybe you’d expect auto-completions to pop up, and get red squiggles on the fly as you’d make edits. When you (and more recently, perhaps an AI agent) were ready to build your project, you’d run tsc, check the output for errors, and then run your generated code somehow.

A faster TypeScript means every part above is streamlined. Waiting for your editor to fully load your project will feel instantaneous. Delays on find-all-references, auto-completion, and diagnostics should take a fraction of the time they used to. And when you run tsc, maybe in --watch mode, you’ll be able to tighten your feedback loop and iterate faster than ever before.

You can see this on real-world projects. In fact, you can try comparing on a few open-source projects yourself. Here are the build times of running TypeScript 6 and 7 on some fairly large open source codebases.

Codebase TypeScript 6 TypeScript 7 Speedup
vscode 125.7s 10.6s 11.9x
sentry 139.8s 15.7s 8.9x
bluesky 24.3s 2.8s 8.7x
playwright 12.8s 1.47s 8.7x
tldraw 11.2s 1.46s 7.7x

Compile times of the projects between TypeScript 6 and 7 described the table above, ranging from 7.7x to 11.9x

TypeScript 7 also typically does better while asking for less aggregate memory over the span of a build.

Codebase TypeScript 6 TypeScript 7 Memory Delta
vscode 5.2GB 4.2GB -18%
sentry 4.9GB 4.6GB -6%
bluesky 1.8GB 1.3GB -26%
playwright 1.0GB 0.9GB -11%
tldraw 0.6GB 0.5GB -15%

Differences in memory reduction between TypeScript 6 and 7 described in the table above, ranging from -6% to -26%

Of course, there’s more to the experience than the full build. On the same computer, opening a file with an error in the VS Code codebase would previously take about 17.5 seconds from the time you opened the editor to the time you saw the first error. With TypeScript 7, it’s under 1.3 seconds – over 13x faster.

Battle-Tested and Ready for Production

The TypeScript project contains tens of thousands of tests built over more than a decade that run on every commit on our main branch. They’ve ensured every one of our releases is stable and reliable.

But TypeScript 7 is no ordinary release. Beyond our test suite, we’ve leveraged a number of different resources to make sure TypeScript 7 is solid for production use.

Over the last year we’ve worked with many large teams internally and externally to test TypeScript 7 on real-world codebases. The results have been overwhelmingly positive, with entire companies reporting that TypeScript 7 has been stable, fast, and easy to adopt. For example, the VS Code team recently highlighted their experience with TypeScript 7’s preview releases to move faster in their development cycle. We’ve also worked with Microsoft teams like Loop, Office, PowerBI, Teams, and Xbox to ensure that TypeScript is ready for the largest of codebases. Likewise, companies like Bloomberg, Canva, Figma, Google, Lattice, Linear, Miro, Notion, Sentry, Slack, Vanta, Vercel, VoidZero, and more have worked with us to test TypeScript 7 on their codebases and given us feedback to make it better.

Additionally, we’ve rebuilt much of our broader test infrastructure to run on TypeScript 7. TypeScript 6 and earlier had automated and on-demand testing for TypeScript and JavaScript projects on GitHub to detect regressions in the compiler and language service. The same testing is back, and running against TypeScript 7, finding issues in real codebases so we can find gaps in our core test suite and ship a better experience.

The combination of explicit feedback, automated crash reports, and aggressive testing has made a measurable difference in quality. In fact, our data insights have shown us that TypeScript 7.0’s new language server has actually reduced failing language server commands by over 80%, and reduced server crashes by over 60% compared to that of TypeScript 6.0.

We’ve also heard some incredible feedback from teams at scale:

  • Slack engineers have told us that TypeScript 7 eliminated 40% of their merge queue time and brought type-checking time in CI from about 7.5 minutes to 1.25 minutes. Local development in the editor was previously almost “unusable” due to language server load times and engineers would typically let CI do a full type-check. TypeScript 7 has been able to load the same codebase in a few seconds and made local type-checking feasible again.
  • Builds at Vanta have dramatically improved, showing a speedup of up to 9x faster on one of their biggest projects.
  • Similarly, the News Services team at Microsoft told us that adopting TypeScript 7 saved them 400 hours a month waiting for CI builds.
  • Last year, engineers working on PowerBI described TypeScript 7 in the editor as “life-saving” for working on their codebase. They adopted the experience as a default even before TypeScript 7 supported rename functionality in VS Code.
  • Developers working on Loop’s monorepo were also ecstatic. The previous editor experience was described as unusable at their scale, whereas the TypeScript 7 experience has been “amazing” to use.
  • Canva developers have told us that TypeScript 7’s language service shows dramatic speedups, going from about 58 seconds to seeing the first error in their editors to about 4.8 seconds.

Running Side-by-Side with TypeScript 6.0

While TypeScript 7.0 is here, it does not ship with an API. We expect TypeScript 7.1 to ship with a new (and different) API, but until then we have made it a priority to ensure TypeScript can be run side-by-side with TypeScript 6.0 for utilities that still need some programmatic access to the compiler (such as typescript-eslint).

As part of the 6.0/7.0 transition process, we’ve published a new compatibility package, @typescript/typescript6. This package provides an executable named tsc6, so that if needed, you can install TypeScript 7.0 (which ships its own tsc binary) side-by-side without naming conflicts. The new package also re-exports the TypeScript 6.0 API, so that you can use tsc for TypeScript 7, while other tooling can continue to rely on 6.0.

Because some tools like typescript-eslint expect to import from typescript directly via peer dependencies, we recommend achieving this via npm aliases. You should be able to run the following command

npm install -D typescript@npm:@typescript/typescript6

or modify your package.json as follows:

{
  "devDependencies": {
    "typescript": "npm:@typescript/typescript6@^6.0.2",
  }
}

Note that doing this will leave you only with a tsc6 executable. To get 7.0’s tsc, you can add another alias for TypeScript 7 and npx tsc will just work with 7.0:

{
  "devDependencies": {
    "@typescript/native": "npm:typescript@^7.0.2",
    "typescript": "npm:@typescript/typescript6@^6.0.2"
  }
}

Nightly Builds and @typescript/native-preview

Until now, most developers have installed TypeScript 7 via the @typescript/native-preview package. This package shipped nightly builds of the new codebase, and has served the community well with over 8.5 million weekly downloads!

However, going forward, nightly builds will soon resume under the standard typescript package with the next tag. You can install it with:

npm install -D typescript@next

Custom Scaling: Parallelization and Controls

TypeScript 7.0 now performs many steps in parallel, including parsing, type-checking, and emitting. Some of these steps, like parsing and emitting can mostly be done independently across files. As such, parallelization automatically scales well with larger codebases with relatively little overhead. But not every step in a TypeScript build is easily parallelizable.

TypeScript 7 introduces the experimental --checkers and --builders flags to fine-tune the parallelization behavior for less-trivial steps like type-checking and project reference building. It also introduces a --singleThreaded flag to disable parallelization entirely, which can be useful for debugging or running in environments with limited resources.

Type-Checker Parallelization

Other steps, like type-checking, have more complex dependencies across files. Most files end up relying on the same type information from their dependencies and the global scope, and so running type-checkers completely independently would be wasteful – both in computation and memory. On the other hand, type-checking occasionally relies on the relative ordering of information in a program, and so type-checking from scratch must always check the same files in an identical order to ensure the same results.

To enable parallelization while avoiding these pitfalls, TypeScript 7.0 creates a fixed number of type-checker workers with their own view of the world. These type-checking workers may end up duplicating some common work, but given the same input files, they will always divide them identically and produce the same results.

The default number of type-checking workers is 4, but it can be configured with the new --checkers flag. You may find that increasing this number can further speed up builds on larger codebases where typical machines have more CPU cores, but will typically come at the cost of increased memory usage. For example, in the table above, we ran TypeScript 7 with its default of --checkers 4. Here’s what the results look like on the same machine with --checkers 8.

Codebase TypeScript 6 TypeScript 7 (--checkers 8) Speedup
vscode 125.7s 7.51s 16.7x
sentry 139.8s 12.08s 11.6x
bluesky 24.3s 2.01s 12.1x
playwright 12.8s 1.16s 11x
tldraw 11.2s 1.06s 10.6x

As you can see, these codebases get a better speedup from dedicating more cores, but results will differ across projects and underlying machines.

On the other hand, on machines with fewer CPU cores and less memory (e.g. CI runners) you may want to decrease this number to avoid unnecessary or incidental overhead. You can specify a value as low as --checkers 1, effectively making type-checking single-threaded and eliminating duplicate work.

In rare cases, varying the number of --checkers may surface order-dependent results. Specifying a fixed number of checkers across build environments can help ensure everyone is getting the same results, but is up to the discretion of your team.

Project Reference Builder Parallelization

TypeScript 7.0 can parallelize builds within a project, but it can now also build multiple projects at once as well. This behavior can be configured with the new --builders flag, which controls the number of parallel project reference builders that can run at once when running under --build. This can be particularly helpful for monorepos with many projects.

Like --checkers, increasing the number of builders can speed up builds, but may come at the cost of increased memory usage. It also has a multiplicative effect with --checkers, so it’s important to find the right balance for your machine and codebase. For example, building with --checkers 4 --builders 4 allows up to 16 type-checkers to run at once, which may be excessive.

Unlike --checkers, varying the number of builders should not produce different results; however, building project references is fundamentally bottlenecked by the dependency graph of projects (with the exception of type-checking on codebases that leverage --isolatedDeclarations and separate syntactic declaration file emit).

Single-Threaded Mode

In some cases, it can be helpful to enforce single-threaded operation throughout the compiler. This may be useful for debugging, comparing performance with TypeScript 6 and 7, when orchestrating parallel builds externally, or for running in environments with very limited resources. To enable single-threaded mode, you can use the new --singleThreaded flag. This will not only cap the number of type-checking workers to 1, but also ensure parsing and emitting are done in a single thread.

Improved --watch Mode

TypeScript 7 ships with a completely rebuilt --watch mode. --watch is now powered by a new foundation based on the Parcel bundler’s file-watcher that provides efficient and stable cross-platform file watching capabilities.

When our team set out to port our file watching logic, we encountered a few challenges with cross-platform file watching in Go. The standard library doesn’t provide a built-in file watching API, and existing third-party libraries we explored had various issues with stability, performance, cross-platform support, or issues with build tooling integration. We were able to build solutions around polling periodically to check for file changes, and this worked broadly across operating systems; however it was computationally expensive, especially at larger-scale projects with many dependencies in node_modules. Even with dynamic scheduling strategies, we found that pure-polling solutions were too taxing for general use.

For many years, Visual Studio Code has relied on @parcel/watcher, and in recent years TypeScript in VS Code has relied on its file watching capabilities indirectly. While it seemed promising, one of the problems for us with Parcel’s watcher is that it’s written in C++, and in turn requires a full C++ toolchain to build. Given our positive experience with Parcel’s watcher in VS Code, we explored porting it to Go with a few minimal assembly shims to avoid introducing a new toolchain dependency.

The exploration has been a success – what started as a very direct translation from C++ to Go was further refined into idiomatic Go that still passes the ported test suite. The watcher is a self-contained package that has allowed us to keep a clean separation of concerns between what we care to watch and why. We are now seeing significant resource improvements in --watch mode across platforms, and have been hearing positive feedback from earlier users of TypeScript 7.

We’d like to extend our thanks to Devon Govett whose work on Parcel has provided immense benefits to both the Visual Studio Code and TypeScript projects. We hope this port will provide opportunities and insights for the original Parcel watcher codebase over time.

Updates Since 5.x, and New Behaviors from 6.0

TypeScript 7.0 is made to be compatible with TypeScript 6.0’s type-checking and command-line behavior. Practically any TypeScript code that compiles cleanly with TypeScript 6.0 (with the stableTypeOrdering flag on, and without any ignoreDeprecations flag set) should compile identically in TypeScript 7.0.

With that said, TypeScript 7.0 adopts 6.0’s new defaults, and provides hard errors in the face of any flags and constructs deprecated in TypeScript 6.0. This is notable as 6.0 is still relatively new, and many projects will need to adapt to its new behaviors. We encourage developers to adopt TypeScript 6.0 to make the transition to TypeScript 7.0 easier, and you can also read the TypeScript 6.0 release blog post for more details on these deprecations.

At a glance, the notable default changes to configuration are:

  • strict is true by default.
  • module defaults to esnext.
  • target defaults to the current stable ECMAScript version immediately preceding esnext.
  • noUncheckedSideEffectImports is true by default.
  • libReplacement is false by default.
  • stableTypeOrdering is true by default, and cannot be turned off.
  • rootDir now defaults to ./, and inner source directories must be explicitly set.
  • types now defaults to [], and the old behavior can be restored by setting it to ["*"].

We believe the rootDir and types changes may be the most “surprising” changes, but they can be mitigated easily. Projects where the tsconfig.json sits outside of a directory like src will simply need to include rootDir to preserve the same directory structure.

  {
      "compilerOptions": {
          // ...
+         "rootDir": "./src"
      },
      "include": ["./src"]
  }

For the types change, projects that depend on specific global declarations will need to list them explicitly. For example,

  {
      "compilerOptions": {
          // Explicitly list the @types packages you need (e.g. bun, mocha, jasmine, etc.)
+         "types": ["node", "jest"]
      }
  }

The deprecations that have turned into hard errors with no-op behavior are:

  • target: es5 is no longer supported.
  • downlevelIteration is no longer supported.
  • moduleResolution: node/node10 are no longer supported, with nodenext and bundler being recommended instead.
  • module: amd, umd, systemjs, none are no longer supported, with esnext or preserve being recommended in conjunction with bundlers or browser-based module resolution.
  • baseUrl is no longer supported, and paths can be updated to be relative to the project root instead of baseUrl.
  • moduleResolution: classic is no longer supported, and bundler or nodenext are the recommended replacements.
  • esModuleInterop and allowSyntheticDefaultImports cannot be set to false.
  • alwaysStrict is assumed to be true and can no longer be set to false.
  • The module keyword cannot be used in namespace declarations.
  • The asserts keyword cannot be used on imports, and must use the with keyword instead (to align with developments on ECMAScript’s import attribute syntax).
  • /// <reference no-default-lib /> directives are no longer respected under skipDefaultLibCheck.
  • Command line builds cannot take file paths when the current directory contains a tsconfig.json file unless passed an explicit --ignoreConfig flag.

Template Literal Types Now Preserve Unicode Code Points

TypeScript 7.0 now treats Unicode code points more naturally when inferring from template literal types. For example:

type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never;

type Result = HeadTail<"😀abc">;
//   ^
// In 7.0: ["😀", "abc"]
// Previously: ["\ud83d", "\ude00abc"]

Previously, TypeScript followed JavaScript’s UTF-16 indexing behavior here and split "😀" into two halves of a surrogate pair (\ud83d and \ude00). That was technically consistent with indexing in JavaScript (e.g. the inferred Head type was equal to "😀abc"[0]), but it usually wasn’t what people intended, and could produce string literal types containing unpaired surrogates that aren’t semantically meaningful.

This is a breaking change for type-level string manipulation that intentionally modeled UTF-16 code units, such as some string Length utilities. In practice, we expect the new behavior to be more useful and less surprising: template literal inference now follows the same intuition as iterating a string with for...of or spreading it with [...str], where "😀" is treated as one unit.

JavaScript Differences

As we ported the existing codebase, we also took the opportunity to revisit how our JavaScript support works.

TypeScript originally supported JavaScript files by using JSDoc comments and recognizing certain code patterns for analysis and type inference. Lots of the time, this was based on popular coding patterns, but occasionally it was based on whatever people might be writing that Closure and the JSDoc doc generating tool might understand. While this approach was helpful for developers with loosely-written JSDoc codebases, it required a number of compromises and special cases to work well, and diverged in a number of ways from TypeScript’s analysis in .ts files.

In TypeScript 7.0, we have reworked our JavaScript support to be more consistent with how we analyze TypeScript files. Some of the differences include:

  • Values cannot be used where types are expected – instead, write typeof someValue
  • @enum is not specially recognized anymore – create a @typedef on (typeof YourEnumDeclaration)[keyof typeof YourEnumDeclaration].
  • A standalone ? is no longer usable as a type – use any instead.
  • @class does not make a function a constructor – use a class declaration instead.
  • Postfix ! is not supported – just use T.
  • Type names must be defined within a @typedef tag (i.e. /** @typedef {T} TypeAliasName */), not adjacent to an identifier (i.e. /** @typedef {T} */ TypeAliasName;).
  • Closure-style function syntax (e.g. function(string): void) is no longer supported – use TypeScript shorthands instead (e.g. (s: string) => void).

Additionally, some JavaScript patterns, like aliasing this and reassigning the entirety of a function’s prototype are no longer specially treated.

While some of our JS support is in flux, we have been updating this CHANGES.md file to capture the differences between TypeScript 6.0 and 7.0 in more detail.

Editor Experience

As we mentioned above, TypeScript 7.0’s performance improvements are not limited to the command line experience – they also extend to the editor experience too. For VS Code users, we have a dedicated extension for TypeScript 7. When you install this extension, it will automatically become the default experience. You can disable and re-enable it at any time with the “Disable TypeScript 7 Language Server” and “Enable TypeScript 7 Language Server” commands from the command palette. In the coming weeks support for TypeScript 7 will ship as part of VS Code itself.

For Visual Studio users, the latest version of the IDE will automatically enable TypeScript 7 based on your workspace. You won’t need to do anything differently.

Of course, TypeScript 7 should work great in any editor of your choosing. The new foundation is built on the Language Server Protocol (LSP) and is able to leverage multiple threads to serve simultaneous requests as quickly as possible.

Since it first debuted, we’ve added in missing functionality like auto-imports, expandable hovers, inlay hints, code lenses, go-to-source-definition, JSX linked editing and tag completions, and more. Missing features from TypeScript 7.0 beta, such as semantic highlighting, “sort imports”, “remove unused imports”, and more are now in.

Additionally, we’ve continued to drive performance and stability in the past few months. We’ve rebuilt much of our testing and diagnostics infrastructure to make sure the quality bar is high, in which we are able to fuzz-test the language server against the top TypeScript and JavaScript codebases on GitHub. As we’ve mentioned above, TypeScript 7’s new language server is significantly more stable than TypeScript 6’s.

TypeScript and Embedded Languages

It’s worth calling out that workflows that use Vue, MDX, Astro, Svelte, and others will likely not yet be able to leverage TypeScript 7. Similarly, specialized type-checking within templates like Angular will also likely not use TypeScript 7. This is mainly because TypeScript 7 does not yet expose a stable programmatic API, and so tools (such as Volar) which embed TypeScript into their own compilers and language services can only currently rely on TypeScript 6.0. We expect this to be a point-in-time issue, as we are committed to providing a solution here. We will be actively working with the maintainers of these projects to ensure TypeScript 7 supports these workflows.

Until then, we recommend that teams use TypeScript 7 in scenarios where language server plugins are not required. Projects using Angular can use a combination of TypeScript 7 to get fast project-wide error detection at the CLI with tsc, and TypeScript 6.0 for editor support. Projects using Vue, MDX, Astro, Svelte, and others will need to continue using TypeScript 6.0 for now. In VS Code, users can simply run the “Disable TypeScript 7 Language Server” command to revert to TypeScript 6.0.

The Road Forward

TypeScript 7.0 is a major milestone in the TypeScript project. This port has been the primary focus of our team for over a year, and with 7.0 out, we will be returning to new feature work, ergonomic improvements, more performance wins, and implementing a new API for the broader ecosystem. While that seems major, we expect a fairly similar timeline to releases prior to TypeScript 7.0, with new featureful versions published every 3-4 months. With TypeScript 7.1 on the horizon, we hope to bridge any gaps to help bring the community forward.

We also encourage you to share your experience using TypeScript 7.0 online. Feel free to follow and tag @typescriptlang.org on Bluesky or @typescript@fosstodon.org on Mastodon, or @typescript on Twitter, and let us and others know what you think of TypeScript 7.

We know this new release will be incredibly valuable for the TypeScript ecosystem. We hope that it makes your day-to-day coding experience more fast, fun, productive, and joyful.

Welcome to the native era of the TypeScript toolset.

Happy Hacking!

– The TypeScript Team

The post Announcing TypeScript 7.0 appeared first on TypeScript.

Read the whole story
emrox
5 days ago
reply
Hamburg, Germany
alvinashcraft
5 days ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories