The Road to Responsive IntelliJ-Based IDEs

TL;DR: This is a technical blog post about our work to improve UI responsiveness in IntelliJ-based IDEs. It’s a multi-year effort to fix several architectural constraints. The project is still ongoing, and so far we’ve built new tools and APIs that help move performance-sensitive work away from the UI thread. This change means the UI thread now holds the write lock for much less time, about one-third as long as before. If you are not interested in the technical details, you can skip to the end for the graphs.

Comic showing how read actions and write actions cannot at the same time.

One of the most common complaints about IntelliJ-based IDEs is performance. We know. We’re working to make the IDE more responsive. That is not always easy: The IntelliJ Platform is 25 years old, and some of its architectural decisions are baked in. Those decisions also make some optimizations hard.

The deadly entanglement

Performance is impacted when the UI thread has to take care of business logic

The IntelliJ Platform is a multithreaded framework built around a single read-write (RW) lock. The IDE operates on several core data structures: syntax trees (PSI), the text view of files (the Document subsystem), and a view of the OS file system (the Virtual File System, or VFS). Access to these structures is protected by the RW lock. Operations are divided into read actions and write actions. At any moment, only one write action can exist; multiple read actions can run in parallel, but read actions and a write action cannot run at the same time.

Our IDEs are also UI applications, which means they use a UI framework. In the IntelliJ Platform, that framework is Java AWT, which has a single UI thread: the event dispatch thread (EDT). This thread is responsible for processing user input and painting the UI. Java also allows business logic to run there. The EDT’s performance directly affects how responsive the application feels: If it can process paint events and user input quickly, the IDE feels snappy.

This is where the freezes come from. The write action itself can cause freezes. Some write actions, such as reparsing syntax trees or updating file system views, are expensive on their own. Another, less obvious, source of freezes is waiting to acquire the write lock. Since read actions and write actions cannot run simultaneously, starting a write action means waiting for all active read actions to finish. We’ve put a lot of work into making read actions cancelable, but the problem doesn’t go away entirely: If even one read action is uncooperative, the whole IDE can suffer.

That naturally led us to identifying a key goal: Move write actions off the UI thread.

With good intentions

The IntelliJ Platform team has to work around old editor code

The effort to support background write actions began in 2019, with Valentin Fondaratov, Andrew Kozlov, and Peter Gromov.

For a long time, code running on the EDT could conveniently access IntelliJ Platform models. With background write actions, that convenience becomes a problem: UI code can no longer assume that model access is always safe without explicit coordination. To preserve compatibility, we had to keep a lot of existing UI code working while we made those assumptions explicit.

There was another complication. Code on the EDT could also start a write action immediately. That is not true for ordinary explicit read actions, because a write action cannot simply begin in the middle of a read action.

This is where write-intent comes in. Write-intent is a lock state that still allows parallel read actions, but can be held by only one thread at a time and can be upgraded atomically to a full write action. That makes it a good fit for EDT code that may need to transition into a write action. Adopting this approach in the platform was an important step toward supporting background writes while preserving existing behavior.

The project was paused in 2020 because the amount of required change was enormous. Many UI components, especially the editor, relied heavily on long-standing assumptions about model access from the EDT.

The grand refactoring

A lion doesn't concern himself with legacy code

The project was paused, but not abandoned. In 2022, Lev Serebryakov and Daniil Ovchinnikov restarted the effort.

During this stage, we refactored the IntelliJ Platform to surface a lot of assumptions the platform had relied on implicitly. That work reduced some of the platform’s reliance on implicit locking in UI-driven code paths.

Another important part of this stage was our collaboration with the JetBrains Research team. Our previous lock implementation assumed write actions ran only on the EDT. Moving them into the background needed a different lock, and plain old ReentrantReadWriteLock was not a good fit for our needs. The result was a new cancelable lock that now powers the platform (see this research paper).

This stage lasted until the end of 2024.

When one lock is not enough

Having too many locks for reading and writing is confusing

At the beginning of 2025, Konstantin Nisht took over this part of the project. At that point, we were almost ready to run our first background write actions. But there was one major problem left: modality.

Some UI elements in the IDE need to block the user’s attempts to interact with anything but themselves. These are modal dialogs, such as the Settings dialog. In the IntelliJ Platform, modality also affects the model: While a modal dialog is visible, unrelated write actions should not be able to start. Historically, the EDT scheduler handled much of this by ensuring that UI work launched in a non-modal context would simply not run while a modal dialog was active.

Background write actions do not fit into that model automatically.

If a modal dialog is shown on the EDT while holding write-intent, then a naive attempt to run a background write action can deadlock. At the same time, we still want computations inside the dialog to make progress without being disrupted by unrelated work outside it.

To solve this, we introduced a modality-aware locking strategy that separates what happens inside a modal dialog from what happens outside it. That preserves the guarantees that modal dialogs rely on and lets background write actions run.

This also works for nested modal computations, which matters because real modal workflows are not always flat. With that in place, we were finally able to run our first write actions in the background.

Moving work without breaking plugins

Trying not to break existing code with drastic changes

The initial write actions were relatively easy to move into the background. They lived in the Workspace Model and were primarily used to invalidate some caches. After that, it was time to try something more substantial: VFS refresh.

VFS refresh is the process of synchronizing file modification events from the operating system with the IDE’s internal data structures. Besides applying those events, refresh also calls listeners, meaning plugin code that reacts to file system changes. Traditionally, VFS refresh runs in a write action, and those listeners are called there too.

That creates a compatibility problem. For many years, a large amount of listener code has assumed that it would run on the EDT. Some listeners naturally access the UI. Many of them live in plugins we do not control, which means we cannot simply change the execution model and hope everything keeps working.

So the challenge was not only to move the write action itself, but to do it without breaking a long tail of existing plugin code.

The basic idea was straightforward: Keep the write action in the background, but hand specific listener work back to the EDT when compatibility requires it. Swing gives us a synchronous handoff for that with invokeAndWait(...).

Unfortunately, there is a deadlock hiding behind that seemingly simple approach. If the background write action tries to synchronously hand work to the EDT while the EDT is itself blocked waiting on a lock, the IDE can freeze.

To avoid that, we introduced an internal compatibility mechanism that allows carefully selected UI events to keep making progress during these waits. That gave us a way to migrate this incrementally: We could move expensive write work off the EDT while preserving compatibility for listeners that still depended on it.

This turned out to be one of the most important parts of the project. It let us migrate listeners incrementally, keep compatibility for external plugins, and still get most of the performance win by moving the slowest pieces first.

After VFS refresh, we migrated the document commit process as well. This is the process that rebuilds PSI from documents, and once the core write-action machinery was in place, moving it was much more straightforward.

Let’s do it later

Comic about how certain tasks need to be done later and no matter what you chose, it has drawbacks

Background write actions are not a cure-all. They help reduce the time the EDT spends running write actions, but they do not automatically eliminate the time the EDT spends waiting for locks.

Even if write actions run in the background, the EDT can still be asked to acquire read or write-intent access. While a write action is running, or while one is waiting to acquire the write lock, those requests can still freeze the UI. That brings us to the second part of this project: removing as much lock acquisition from the EDT as possible.

One especially problematic area was the editor. The editor is responsible for drawing content on the screen based on its models, such as the caret, foldings, and document text. But document modification is protected by the RW lock, and the editor still needs access to that data on the EDT. For a long time, this meant read actions were everywhere in the editor, including in paint paths. That is a major problem, because painting can happen at any time, which means the editor could end up demanding read access at exactly the moment we most want the UI thread to stay free.

In this area, we made a pragmatic trade-off. We relaxed some lock requirements for editor-related EDT paths, while keeping some document-related writes on the EDT to preserve consistency. That made editor painting less lock-heavy, even though it did not yet let us move all document modification into the background. We still have to solve that part.

Another source of lock pressure on the EDT was our API for asynchronous computations. To preserve compatibility, many such computations still ended up coupled to write-intent acquisition, which meant they could freeze the EDT at unpredictable moments.

Here, the key observation was simple: If someone schedules work to run asynchronously on the UI thread, that person usually does not care about the exact microsecond when it starts. That means we do not always need to block the EDT waiting for write-intent access. In many cases, we can just delay the computation until that access becomes available. After some changes to the platform’s UI scheduling, this became much less of a problem.

Results, future work, and acknowledgments

Background write actions are complex because they touch fundamental contracts in the IntelliJ Platform. We’re still building APIs and tools to help plugins decouple their logic from the EDT. The work is not finished, but here’s where we are.

As a metric, we track how much time the EDT spends doing write actions. Here is the graph, based on data gathered one week after each release:

UI write action performance comparison between version 2025.2 and 2025.3

For example, in 2025.2, 1% of users spent 5% of their UI time on write actions. In 2025.3, the same percentile of users spent only 3%. Overall, the expected share of UI time spent on write locks on the EDT dropped from 1.8276% in 2025.2 to 0.5298% in 2025.3.

In the future, our work will focus on removing more write-intent usage from the EDT. We want to eliminate locking from common interactions such as typing. That is a difficult goal because it requires rethinking fundamental structures such as Actions, PSI, and Documents. It’s hard, but we think it’s doable.

Finally, we’d like to thank all the people we haven’t already mentioned who were directly or indirectly involved in this project: Anna Saklakova, Dmitrii Batkovich, Vladimir Krivosheev, Moncef Slimani, Lev Serebryakov, and Nikita Koval, amongst others. This post began as an internal article by Konstantin Nisht and was adapted for the public blog by Patrick Scheibe – with fewer breaking changes than usual.

I Ignored MCP Servers at First, Here’s Why That Was a Mistake

At first, I treated MCP servers like just another buzzword in the growing pile of AI and developer tooling terms.

I assumed it was one more trend that people were repeating before most of them even understood what it meant.

But the more I looked into it, the more obvious it became that ignoring MCP servers was a mistake.

Not because of hype.
Because of where modern tooling is going.

Why MCP servers matter

The real shift is not just AI itself.
The shift is how tools, agents, and systems connect to real capabilities.

That is where MCP becomes interesting.

Instead of thinking only in terms of APIs, dashboards, and isolated tools, developers now need to think about how external capabilities are exposed in structured, reusable ways for intelligent systems and automated workflows.

That is a meaningful change.

What changed my mind

Three things stood out.

1. Tool connectivity is becoming a bigger part of modern development

Building software is no longer just about writing isolated app logic.

It is increasingly about connecting systems, workflows, and services in ways that are composable and easier to automate.

2. Agents need reliable interfaces to real tools

If AI agents are going to do anything useful in the real world, they need a stable way to access tools and context.

That makes this space important for developers, even if they are not building “AI products” directly.

3. Early understanding creates leverage

A lot of developers wait too long before paying attention to infrastructure shifts.

Then suddenly the ecosystem changes, client expectations change, tools change, and they are playing catch-up.

Why developers should care now

Even if you are mainly a frontend or full-stack developer, this still matters.

Because the stack is changing around you.

Clients, teams, and platforms are slowly moving toward systems that are more connected, more automated, and more agent-compatible.

Developers who understand those shifts early usually build better solutions and make better decisions.

Final thought

You do not need to become an expert overnight.

But ignoring MCP servers completely is probably the wrong move.

The better move is to understand why they are emerging, what problem they are trying to solve, and how they fit into the future of developer tooling.

Frontend Performance That Actually Moves the Needle

In this article we’ll cover why Lighthouse scores alone don’t tell the full story, what metrics actually matter at scale, how real user monitoring changes the way you think about performance, and the optimizations that have the most impact on platforms serving millions of users.

Lighthouse is a great tool. I’m not here to tell you to ignore it. But I’ve seen teams chase a perfect Lighthouse score while their real users were experiencing 4-second load times on mid-range android devices with a 4G connection.

The score looked great. The experience wasn’t.

When you’re building for 10 million users, performance stops being about a number in a report. It becomes about real people on real devices with real network conditions. And the gap between a lab score and what your users actually feel is wider than most developers realize.

This article is about closing that gap.

Lighthouse Scores Are Lab Data, Not Reality

Lighthouse runs in a controlled environment. Throttled CPU, simulated network, a clean browser with no extensions, no cached data, no background tabs. That’s not how your users browse.

Your users are on a 3-year-old phone with 15 browser tabs open, on a train with patchy network, while your JavaScript is fighting with a background app for CPU time.

This is why a 90+ Lighthouse score can still result in a poor user experience. The lab doesn’t lie, but it only tells you part of the truth.

Lab data: what Lighthouse gives you – is useful for catching regressions and tracking trends over time. But it should never be your only signal.

Field data: what your real users experience – is where performance work actually pays off.

The Metrics That Actually Matter at Scale

Core Web Vitals

Google’s Core Web Vitals are the closest thing we have to a standardized set of user-centric performance metrics. Three of them matter most:

LCP – Largest Contentful Paint: How long does it take for the largest visible element to render? For most platforms this is a hero image, a video thumbnail or a headline. This is what the user perceives as “the page loaded.”

Target: under 2.5 seconds.

INP – Interaction to Next Paint: How quickly does the page respond after a user interaction? Click a button, tap a menu, submit a form – how long before the page visually responds? This replaced FID (First Input Delay) in 2024 and is a much better measure of real interactivity.

Target: under 200 milliseconds.

CLS – Cumulative Layout Shift How much does the page jump around while loading? Ads loading late, images without dimensions, fonts swapping – these all contribute to CLS. On a content-heavy platform this can quietly destroy the user experience.

Target: under 0.1.

These three metrics directly impact SEO ranking and user retention. At scale, a 0.1 improvement in CLS or a 500ms reduction in LCP translates to measurable engagement and conversion improvements.

TTFB – Time to First Byte

Before the browser can render anything, it needs a response from the server. TTFB measures that wait time. High TTFB usually points to server-side issues – slow API responses, no CDN or unoptimized server rendering.

On platforms with a global audience, CDN configuration alone can cut TTFB from 800ms to under 100ms for a significant portion of your users.

TTI – Time to Interactive

When can the user actually use the page? Not just see it, but interact with it without the UI freezing. This is where JavaScript bundle size and execution time have the most direct impact.

A page that looks loaded but isn’t responding to clicks is one of the most frustrating experiences a user can have.

Real User Monitoring – The Signal You Can’t Ignore

If you’re only running Lighthouse, you’re flying partially blind. Real User Monitoring (RUM) captures performance data from actual user sessions and sends it back to you.

The difference is significant. With RUM you can see:

  • How performance varies by device type, browser and geography
  • Which pages have the worst real-world LCP or INP
  • How performance degrades over time as your codebase grows
  • What percentage of your users are experiencing poor performance right now

Tools like Datadog RUM, SpeedCurve, Mux Data or even the free Chrome User Experience Report (CrUX) give you this visibility.

On a platform serving millions of users, even if 5% of your users are experiencing poor performance, that’s 500,000 people having a bad time. RUM makes that visible. Lighthouse doesn’t.

The Optimizations That Actually Move the Needle

1. JavaScript Bundle Size

This is almost always the biggest lever. JavaScript is the most expensive resource on the web – it has to be downloaded, parsed, and executed before it does anything useful.

Code splitting is non-negotiable at scale. Every route should load only the JavaScript it needs.

// Instead of importing everything upfront
import HeavyComponent from './HeavyComponent'

// Load it only when needed
const HeavyComponent = React.lazy(() => import('./HeavyComponent'))

Audit your bundle regularly. Tools like webpack-bundle-analyzer or vite-bundle-visualizer will show you exactly what’s in your bundle and where the weight is coming from. You will almost always find something surprising.

Third-party scripts are usually the worst offenders. Analytics, chat widgets, ad scripts – these are often loaded synchronously and block rendering. Load them async or defer them entirely until after the page is interactive.

2. Image Optimization

Images are the largest assets on most pages. Getting this wrong has a direct impact on LCP.

  • Use modern formats. WebP is widely supported and significantly smaller than JPEG or PNG. AVIF is even better where supported.
  • Always set explicit width and height on images. This prevents layout shift and helps the browser allocate space before the image loads.
  • Use lazy loading for images below the fold.
  • Serve appropriately sized images. Don’t serve a 2000px wide image to a 400px wide mobile screen.
<img
  src="thumbnail.webp"
  width="400"
  height="225"
  loading="lazy"
  alt="Video thumbnail"
/>

For a platform with a large content library, image optimization alone can reduce page weight by 40–60%.

3. Critical Rendering Path

The browser has to download your HTML, parse it, discover CSS and JavaScript, download those, parse them and then render the page. Every step in that chain is an opportunity to either speed things up or slow things down.

  • Inline critical CSS – the styles needed to render above-the-fold content – directly in the HTML. This eliminates a render-blocking network request for the initial view.

  • Preload key resources the browser won’t discover until late in the parsing process.

<link rel="preload" as="font" href="/fonts/main.woff2" crossorigin>
<link rel="preload" as="image" href="/hero.webp">
  • Defer non-critical JavaScript. If a script doesn’t need to run before the page is interactive, it shouldn’t block rendering.

4. Caching Strategy

I covered this in depth in the previous article in this series, but it’s worth mentioning here because caching is one of the highest-impact performance optimizations available to you.

Repeat visitors on a well-cached platform can load pages almost entirely from cache. No network requests for static assets, no server round trips for unchanged resources. The performance improvement for returning users is dramatic.

If you haven’t read the caching article yet, it’s worth going back to.

5. Reducing Main Thread Work

INP and TTI both suffer when the main thread is busy. JavaScript execution, long tasks, layout recalculations – these all compete for the same thread that handles user interactions.

A few things that help:

  • Break up long tasks. Any task that takes more than 50ms can cause noticeable jank. Use setTimeout or scheduler.postTask to yield control back to the browser between chunks of work.

  • Avoid layout thrashing. Reading and writing to the DOM in alternating calls forces the browser to recalculate layout repeatedly. Batch your reads and writes.

  • Move heavy computation off the main thread with Web Workers.

// Yielding to the browser between heavy tasks
async function processLargeDataset(items) {
  for (let i = 0; i < items.length; i++) {
    process(items[i])
    if (i % 100 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0))
    }
  }
}

Performance Budgets – Making It Stick

One of the hardest parts of performance work at scale is keeping improvements from regressing over time. A performance budget solves this.

A performance budget sets explicit limits on metrics like bundle size, LCP or TTI. If a pull request would push you over the budget, it fails the build.

{
  "resourceSizes": [
    { "resourceType": "script", "budget": 300 },
    { "resourceType": "total", "budget": 1000 }
  ],
  "timings": [
    { "metric": "first-contentful-paint", "budget": 1500 },
    { "metric": "interactive", "budget": 3500 }
  ]
}

This keeps performance on everyone’s radar, not just the engineer who cared enough to optimize it once.

What Scale Actually Teaches You About Performance

Here’s what I’ve learned from working on platforms at this size that you don’t find in most performance guides:

Device distribution matters more than you think. Your development machine is not representative of your users. Profile on a mid-range Android device and you will find issues you never knew existed.

Geography matters. A platform with a global audience needs a CDN strategy, not just a fast server in one region. Network latency from a distant origin server can add seconds to TTFB for users in certain regions.

Performance degrades gradually. Nobody ships a slow app intentionally. It gets slow one dependency, one feature, one third-party script at a time. Without a budget and regular monitoring, you won’t notice until users are already complaining.

The 80/20 rule applies. A small number of pages usually account for the majority of your traffic. Find those pages, measure them obsessively and optimize them first. That’s where your performance work will have the most impact.

Final Thoughts

Lighthouse is a tool, not a goal. A green score means you’ve done the basics right. It doesn’t mean your users are having a fast experience.

The teams that get performance right at scale are the ones who measure what their real users experience, set budgets to prevent regression and focus their effort on the optimizations that actually move the needle for their specific platform and audience.

Start with RUM. Find where your real users are struggling. Fix those things first.

The Lighthouse score will follow.

Have thoughts or questions on frontend performance? Drop them in the comments, always happy to discuss.

This article is part of the Frontend at Scale series.

The New Developer Stack Is Changing Fast, Here’s What You Should Learn First

It is getting harder to answer a simple question:

What should developers learn first right now?

The stack keeps moving.
Tools change quickly.
New ideas appear every month.
And the internet is full of conflicting advice.

So instead of trying to learn everything, it makes more sense to focus on what creates real leverage.

1. Strong frontend fundamentals still matter

React, Next.js, state management, rendering behavior, data fetching, and performance still matter.

A lot of new tooling comes and goes, but strong frontend understanding keeps paying off.

2. Performance is no longer optional

Slow websites hurt UX, SEO, and conversion.

Developers who know how to improve Core Web Vitals, loading strategy, rendering decisions, and frontend efficiency are solving real business problems.

3. Technical SEO is becoming more valuable

Many developers still treat SEO like a marketing-only topic.

That is a mistake.

Technical SEO overlaps with:

  • site architecture
  • rendering
  • performance
  • crawlability
  • indexing
  • metadata

That means developers who understand it create better products.

4. AI workflow literacy is becoming important

Even if you are not building AI-first products, understanding agents, workflows, tools, and automation is becoming increasingly relevant.

This is quickly moving from optional curiosity to practical advantage.

5. System thinking matters more than tool chasing

The real goal is not to memorize every new tool.

The goal is to understand:

  • why the tool exists
  • what problem it solves
  • where it fits in a real system

That way, you build durable skill instead of short-term hype knowledge.

Final thought

The stack is changing fast, but the smartest move is not panic.

It is prioritization.

Learn the parts that create long-term technical leverage, solve real problems, and increase the value you can bring to real products.

If you care about modern web development, performance, and practical implementation, I write about these topics regularly.

Website: https://salmanizhar.com

Everyone Is Talking About AI Workflows, But Very Few Developers Actually Understand Them

AI workflows have become one of the most repeated ideas in modern tech conversations.

Everywhere you look, people are talking about automation, agents, workflow builders, orchestration, and AI-powered systems.

But there is a problem.

A lot of developers still do not really understand what a useful AI workflow looks like in practice.

Why this matters

Without a real understanding of workflows, teams end up doing one of two things:

  • building shallow demos
  • overcomplicating everything

Neither creates long-term value.

What a real AI workflow usually involves

A useful workflow is not just “send prompt, get answer.”

It usually includes some combination of:

  • input collection
  • context retrieval
  • decision logic
  • tool usage
  • output formatting
  • approval or execution steps

That means good workflows require more than prompt writing.

They require system thinking.

Why developers struggle here

1. They focus too much on the model

The model matters, but it is only one part of the workflow.

A weak system around a strong model still produces weak results.

2. They ignore operations

Many useful workflows depend on timing, retries, approvals, error handling, and tracking.

That means workflow design is not just about intelligence.
It is also about reliability.

3. They underestimate context and tool quality

An AI workflow is only as good as the information and capabilities it can access.

That is why context design and tool integration are so important.

Final thought

The future will not belong only to developers who know how to call an API.

It will belong to developers who know how to design systems that combine models, tools, context, and execution in useful ways.

That is what makes AI workflows worth understanding properly.

If you care about modern web development, performance, and practical implementation, I write about these topics regularly.

Website: https://salmanizhar.com

WSJF, Cost of Delay and Why Your Loudest Stakeholder Keeps Winning the Backlog Argument

A few months ago I sat in a PI planning session where a team of twelve argued for forty minutes about whether a “see product alternatives” button was more important than a “notify me” button for out-of-stock items. Both PMs had slide decks. Both had data. Both were convinced. The conversation ended the way those conversations always end; the loudest person in the room won, and everyone else went back to their laptops with a quiet grudge.

That meeting is why I rebuilt our WSJF Calculator. Not because the world needed yet another backlog prioritization tool, but because I kept watching smart teams flush an entire quarter down the drain arguing about feelings when the math has existed since the 1980s.

The trick nobody tells you about prioritization

Most people think prioritization is about picking the most valuable thing. It isn’t. It’s about picking the thing where value divided by size is highest. That distinction sounds pedantic until you realize it’s the difference between shipping eight small wins in a quarter and shipping one lumbering “strategic initiative” that nobody really wanted.

That’s the whole thesis behind Weighted Shortest Job First. You score each backlog item on two things; how expensive it is to delay, and how big the job is. Then you divide. The highest score wins. That’s it. No story point debates at 9am on a Monday, no “is login page a 3 or a 5”, no pretending your gut feeling is a methodology.

WSJF = Cost of Delay / Job Size

Don Reinertsen popularized this in The Principles of Product Development Flow, which I’d call the best agile book ever written if the phrase “agile book” didn’t immediately make most engineers reach for the nearest window. It’s actually a queueing theory book dressed up as a management book, which is why it’s so good.

Cost of Delay is three things, not one

Here’s where most WSJF spreadsheets get it wrong. Cost of Delay isn’t a single number. It’s the sum of three very different questions:

  • Business Value. If we shipped this tomorrow, how much would users or the business care? Revenue, retention, a specific OKR. Concrete when you can, relative when you can’t.
  • Time Criticality. Does the value decay over time? A Black Friday feature shipped in December is worth approximately zero. A compliance deadline you miss is worth less than zero.
  • Risk Reduction / Opportunity Enablement. Does this unlock something else, or take uncertainty off the table? Boring infrastructure work lives here and that’s usually where it gets buried.

Add those three together. That’s Cost of Delay. Divide by Job Size. That’s WSJF. The reason this works isn’t because the numbers are accurate; it’s because the conversation is accurate. You can’t pretend a risk-reduction spike is a revenue feature anymore. Every item has to justify itself on three axes before it gets a score.

Why relative sizing beats hours every single time

The other trap is how you size. People want to estimate in days. Don’t. Human beings are terrible at estimating duration and we have decades of behavioral research showing it; Parkinson’s law alone should be enough to scare you off. What humans are actually okay at is comparing two things and saying “that one’s bigger.”

So score in Fibonacci (1, 2, 3, 5, 8, 13, 20) or T-shirt sizes (XS, S, M, L, XL, XXL). The gaps matter; a jump from 5 to 8 forces a conversation that “4 days vs 5 days” doesn’t. Our tool supports both because teams argue about this, and the argument is not worth having. Pick one. Move on.

WSJF prioritization results: ranked backlog with Business Value, Time Criticality, Risk Reduction, Job Size and WSJF score per item

What the tool actually does

The WSJF Calculator runs in your browser. Nothing leaves your machine. I’m obsessive about this for product-management tools specifically; your roadmap is the kind of thing you really don’t want sitting on someone else’s server.

Here’s what we built into it after watching teams actually use it:

  1. Templates for common domains. E-commerce, SaaS, internal platform, regulated industry. Start from something that already makes sense.
  2. What-if analysis. Inflate the top task’s size by 25%. Does the ranking flip? If yes, your top priority is fragile; re-estimate before you commit.
  3. Robustness indicator. If the top three WSJF scores are within 10% of each other, treat them as equal and let team capacity decide. The tool shows you this in real time so you stop pretending 4.20 is meaningfully bigger than 4.10.
  4. Share links. Copy a URL, paste it in PI planning notes, everyone loads the same backlog state. No Jira exports, no CSVs floating around Slack.
  5. CSV + PDF export. Because some stakeholders only trust a PDF.

When WSJF is the wrong answer

I’ll save you a mistake I’ve made. WSJF is not a universal prioritization framework. It’s the right tool when:

  • You have 15 or more roughly comparable backlog items.
  • Your team is doing continuous delivery with short cycles.
  • Cost of delay is actually variable across items (some are time-sensitive, some aren’t).

It’s the wrong tool when:

  • You have a single strategic bet and three support tickets. Just do the strategic bet.
  • Everything is equally urgent. Then nothing is, and you have a management problem, not a prioritization problem.
  • You’re trying to decide between two products. That’s a Kano or portfolio question.

For binary in/out decisions, reach for MoSCoW or the Eisenhower matrix instead. For consumer-product feature ranking where you want a simpler formula, RICE is friendlier. I built versions of all three; they each solve a slightly different shape of problem.

The meeting ritual that actually works

WSJF breaks down when it becomes a ritual. The calculator does the math; the humans have to do the talking. Here’s the loop I’ve seen work:

  1. Score Cost of Delay first, together. Use planning poker to surface disagreement. If two people scored a 3 and a 13, that’s interesting; dig in.
  2. Score Job Size last. If you know the size first, it anchors the value estimate. Humans are lazy that way.
  3. Read the top three out loud. If anyone flinches, re-score.
  4. Lock and ship. Don’t re-score mid-sprint. The whole point is that yesterday-you and today-you had different gut feelings but the math didn’t move.

For longer-horizon commitments, pair WSJF with probabilistic forecasting. I wrote a whole piece on Monte Carlo delivery forecasting that pairs nicely with this; one tool tells you what to do next, the other tells you when you’ll realistically be done.

The part that surprised me

When I first shipped the calculator, I expected engineers to use it. It turned out product managers are the heaviest users, and the thing they love most isn’t the score. It’s the audit trail. Every ranking has the three sub-scores visible; when a stakeholder asks “why isn’t my feature at the top”, the PM can point at a number and say “because your Time Criticality is S and your Job Size is XL.” The conversation ends in a minute, not a week.

That’s the real point of any prioritization tool. Not to replace judgment. To make judgment visible enough that the loudest person in the room can’t overwrite everyone else.

Try it here: kitmul.com/en/agile-project-management/wsjf-calculator. It’s free, it’s in your browser, and your backlog is still yours.

Introducing Koog Integration for Spring AI: Smarter Orchestration for Your Agents

Spring AI is the application-facing integration layer you may already use. Koog is the next layer up when you need agent orchestration. Spring AI already covers the chat model API, chat memory, and vector storage for RAG, and it provides Spring Boot starters with auto-configuration. Koog’s role is not to erase that, but rather to add a stronger agent runtime, offering:

  • Multi-step strategies and workflows for more precise control.
  • Persistence and checkpoints for fault-tolerant execution.
  • Sophisticated history management for cost-optimization.
  • Automated deterministic planning.

You can now get the best of both worlds. Koog offers seamless Spring AI integration and can be easily layered on top as a higher-level agentic runtime.

Spring AI

If you already use Spring AI, you’re familiar with its broad integration landscape: 13+ LLM providers, 18+ vector databases, and 10+ chat memory backends, all built seamlessly into the Spring ecosystem.

Your application likely already relies on some of these integrations and wasn’t built in isolation. But as your agent’s complexity increases and business requirements demand more reliability, you start needing things that sit above the integration layer, for example, controlled execution logic, guardrails, fault tolerance, and cost optimization. These are the problems Koog was built to solve.

Capability Spring AI Koog
LLM providers ✅ 13+ ✅ 16+
Streaming ✅ Supported ✅ Supported
Tool calling ✅ Supported ✅ Supported
Database integrations ✅ 10+ (e.g. PostgreSQL and MongoDB) Uses the underlying ecosystem with a few integrations provided out of the box (e.g. Postgres) 
Vector databases ✅ 18+ (e.g. Milvus, Weaviate, and PGvector) ✅ Uses underlying integrations
RAG (retrieval-augmented generation) ✅ Supported via advisors and VectorStore ✅ Supported and integrated into agent workflows
Chat memory (short-term) ✅ Supported ✅ Supported
Long-term memory ✅ Supported via vector DB integrations ✅ Built-in and pluggable (semantic and structured memory)
Observability ✅ Basic observability from the Spring ecosystem (Micrometer, etc.), not tailored for LLM or AI observability tooling ✅ OpenTelemetry support, built-in tailored support for popular LLM or AI observability tooling (e.g. Langfuse, W&B Weave, and Datadog)
Parallel execution ❌ Limited, manual ✅ Native (coroutines and concurrent node execution)
Agent strategies ❌ Basic (prompt chaining and tool calling) ✅ Advanced type-safe graph workflows (multi-step reasoning, branching, tool orchestration, domain modeling approach), advanced planners (LLM-based and GOAP)
Persistence ❌ Not built in, only the message history can be saved ✅ Built-in advanced persistence for the agent’s logic and state
History compression ❌ Not built in ✅ Native support with out-of-the-box advanced strategies (summarization, pruning, and token optimization)

The good news is you don’t have to choose one or the other, or dramatically change your existing setup to get there. Koog’s new Spring AI integration lets you keep your current LLM providers and databases exactly as they are, while writing your agents in Koog with minimal configuration changes. Your integration layer stays intact. Koog simply adds a powerful orchestration runtime on top of it.

Let’s take a look at how it works. This post uses a Kotlin and Gradle setup for simplicity, but you can also use the recently released native Java Koog API (and, of course, Maven).

Koog’s Spring AI integration

Let’s say your Spring project already uses three common Spring AI interfaces: ChatModel, ChatMemoryRepository, and VectorStore. Adding Koog on top is just a three-step process.

Step 1: Keep your existing Spring AI dependencies.

// LLM
implementation("org.springframework.ai:spring-ai-starter-model-openai")

// Chat memory
implementation("org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc")

// Vector store
implementation("org.springframework.ai:spring-ai-starter-vector-store-pgvector")

Step 2: Add the Koog integration dependencies.

// Koog
implementation("ai.koog:koog-agents-jvm:0.8.0")

// Bridges ChatModel to Koog's LLMClient / PromptExecutor
implementation("ai.koog:koog-spring-ai-starter-model-chat:0.8.0")

// Bridges ChatMemoryRepository to Koog's ChatHistoryProvider
implementation("ai.koog:koog-spring-ai-starter-chat-memory:0.8.0")

// Bridges VectorStore to Koog's KoogVectorStore
implementation("ai.koog:koog-spring-ai-starter-vector-store:0.8.0")

Step 3: Use the auto-configured Koog beans. Each Koog starter automatically exposes a Spring bean that wraps your existing Spring AI bean:

Spring AI interface Koog bean(s)
ChatModel  PromptExecutor, LLMClient
ChatMemoryRepository ChatHistoryProvider
VectorStore KoogVectorStore

The beans are auto-configured by default when there is a single matching Spring AI candidate, so your existing Spring AI application config stays untouched.

That’s it for setup. Now let’s walk through what you can build. To make things concrete, we’ll use a customer support agent as our running example and progressively add capabilities.

When a pure Spring AI agent reaches its limit

One version of an agent that you could build in pure Spring AI would look like this:

@Service
class CustomerSupportService(
    chatClientBuilder: ChatClient.Builder,
    vectorStore: VectorStore,
    chatMemory: ChatMemory,
) {
    
    // Build a fully configured ChatClient once at construction time
    private val chatClient: ChatClient = chatClientBuilder
        .defaultSystem("""
            You are an e-commerce support assistant.
            Be concise and policy-aware.
            Never invent order data.
            If order context is missing for an order-specific request, ask for it.
        """.trimIndent())
        .defaultAdvisors(
            // Vector store RAG advisor – enriches every prompt with relevant docs
            QuestionAnswerAdvisor(
                vectorStore,
                SearchRequest.builder()
                    .topK(4)
                    .similarityThreshold(0.7)
                    .build()
            ),
            // Sliding-window chat memory advisor – keeps last N turns per session
            MessageChatMemoryAdvisor(chatMemory)
        )
        .build()
    
    suspend fun createAndRunAgent(userPrompt: String, sessionId: String): String? =
        chatClient.prompt()
            .user(userPrompt)
            // Scope memory to session
            .advisorParam(ChatMemory.CONVERSATION_ID, sessionId)
            .call()
            .tools()
            .content()
}

This agent implements a simple tool-calling loop that runs on top of the LLM defined in the config and inserted as a ChatClient. Besides this, the agent has two features. The first is QuestionAnswerAdvisor, which is built on top of VectorStore and behaves like RAG, enriching the conversation with relevant information from external docs. The second is ChatMemory, which keeps only a specified number of messages, helping you control the number of messages in a conversation and save tokens.

But what if we don’t want a window of messages but a message history summary instead? Or, increasing complexity, what if, instead of a primitive tool-calling agentic loop, we wanted a more controllable and tailored strategy with different e-commerce support scenarios, or persistence and durable execution to make our agent fault-tolerant? This is where we reach the limits of Spring AI. But these, and many other agentic features, already exist in Koog and, thanks to the integration, they can easily be built on top of what you’ve already set up for Spring AI in your project

What does Koog’s Spring AI integration enable?

First of all, this is what our e-commerce agent would look like in Koog.

@Service
class CustomerSupportService(
    private val promptExecutor: PromptExecutor,
    private val chatStorage: ChatHistoryProvider,
    private val knowledgeBase: SearchStorage<TextDocument, SimilaritySearchRequest>
) {

    suspend fun createAndRunAgent(userPrompt: String): String {
        val agentConfig = AIAgentConfig(
            prompt = prompt("ecommerce-support") {
                system(
                    """
                        You are an e-commerce support assistant.
                        Be concise and policy-aware.
                        Never invent order data.
                        If order context is missing for an order-specific request, ask for it.
                    """.trimIndent()
                )
            },
            model = OpenAIModels.Chat.GPT5Nano,
            maxAgentIterations = 100
        )

        val toolRegistry = ToolRegistry {
            tools(EcommerceSupportTools())
        }

        val agent = AIAgent(
            promptExecutor = promptExecutor,
            agentConfig = agentConfig,
            toolRegistry = toolRegistry,
	    // Simple tool-calling loop strategy
	    strategy = singeRunStrategy()
        ) {

            // Vector store RAG advisor – enriches every prompt with relevant docs
            install(LongTermMemory) {
                retrieval {
                    storage = knowledgeBase
                    searchStrategy = SimilaritySearchStrategy(
                        topK = 4,
                        similarityThreshold = 0.70
                    )
                    promptAugmenter = UserPromptAugmenter()
                }
            }

            // Sliding-window chat memory advisor – keeps last N turns per session
            install(ChatMemory) {
                chatHistoryProvider = chatStorage
                windowSize(20)
            }
        }

        return agent.run(userPrompt)
    }
}

With Koog’s Spring AI integration, the PromptExecutor bean is auto-configured from your existing Spring AI ChatModel. You inject it directly into your service – no boilerplate configuration class needed.

The same is true for the doc database and chat memory storage features. You don’t need to make any changes to the application config. With Koog beans, they are seamlessly injected into Koog’s LongTermMemory and ChatMemory and used under the hood.

What can you add on top?

Controllable type-safe workflows

Simple LLM loops lack predictability and control for enterprise scenarios. Each iteration is opaque. You can’t branch based on tool results, retry failed steps, or enforce specific conversation flows. For production support agents handling refunds, escalations, or multi-step verifications, you need explicit control over the execution path.

With Koog, in addition to using predefined strategies (such as default loop or ReAct), you can customize a strategy using graphs:

val agent = AIAgent(
    promptExecutor = promptExecutor,
    agentConfig = agentConfig,
    toolRegistry = toolRegistry,
    // Graph strategy, can accept and return anything!
    strategy = strategy<String, String>("ecommerce_support") {
        // Define graph here
    }
)

Instead of putting all of the instructions in a single naive text prompt, the best way to do this is to use structured output and then append an intent-specific prompt to it. This approach reduces the amount of context and gives you more control. 

@SerialName("SupportIntent")
@Serializable
enum class SupportIntent {
    ORDER_STATUS,
    CHANGE_ADDRESS,
    REFUND,
    OTHER
}

@Serializable
@LLMDescription("Normalized support request extracted from a user message.")
data class SupportRequest(
    @property:LLMDescription("Detected support intent")
    val intent: SupportIntent,

    @property:LLMDescription("Order ID if present, otherwise null")
    val orderId: String? = null,
)

val graphStrategy = strategy<String, String>("ecommerce_support") {
    // 1) Detect the intent of the request from user message
    val classifyRequest by nodeLLMRequestStructured<SupportRequest>(
        examples = listOf(
            SupportRequest(
                intent = SupportIntent.ORDER_STATUS,
                orderId = "84721",
                userRequest = "Check the status of order 84721"
            )
        )
    )
}

Once you know the intent, you can append intent-specific instructions or narrow down the required tools and delegate the task to a subgraph with a tool-calling loop:

val graphStrategy = strategy<String, String>("ecommerce_support") {
    ...
    // 2) Check that all request information is provided
    val checkRequest by node<SupportRequest, CheckRequestResult> { request ->
        when {
            request.intent == SupportIntent.OTHER ->
                CheckRequestResult(
                    request = request,
                    needsMoreInfo = true,
                    clarificationQuestion = "Specify the intent: order status, refund, change address?"
                ) 
            ...
            else ->
                CheckRequestResult(
                    request = request,
                    needsMoreInfo = false
                )
        }
    }

    // 3a) Process order status request in separate subgraph with additional prompt (or tools subset)
    val orderStatusFlow by subgraphWithTask<SupportRequest, String>(
        tools = EcommerceSupportTools().asTools()
    ) { req ->
        """
            Handle this request as an ORDER STATUS case.
            Use the order status tool and then answer the user clearly.
            Request: ${req.userRequest}
            Order ID: ${req.orderId}
        """.trimIndent()
    }

    // 3b) Process other intents
    ...
}

Finally, you can organize each step of your workflow into a graph using type-safe edges and conditions that control your agent’s behavior:

val graphStrategy = strategy<String, String>("ecommerce_support") {
    ...
    // Chain all nodes by edges
    edge(nodeStart forwardTo classifyRequest)
    edge(classifyRequest forwardTo checkContext 
        onCondition { it.isSuccess } 
        transformed { it.getOrThrow().data }
    )
    edge(classifyRequest forwardTo nodeFinish 
        onCondition { it.isFailure }
        transformed { "Failed to classify request." }
    )
    // If more information is required
    edge(checkContext forwardTo nodeFinish
        onCondition { it.needsMoreInfo }
        transformed { it.clarificationQuestion }
    )
    // If we know the intent
    edge(checkContext forwardTo orderStatusFlow
        // Add intent == SupportIntent.ORDER_STATUS condition for the transition  
        onCondition { request.intent == SupportIntent.ORDER_STATUS }
        transformed { it.request }
    )
    ...
    edge(orderStatusFlow forwardTo nodeFinish)
}

You have complete freedom to experiment and make the agent as complex as you need it to be.

Persistence (durable execution)

There’s complex logic at play, so you need to be extremely careful not to lose the execution point and state. And thanks to graphs, that’s possible. Just install and configure the Persistence feature, which will also use the data source from Spring!

@Service
class CustomerSupportService(
    private val dataSource: DataSource,
    ...
) {
    ...
    val agent = AIAgent(
        ...
    ) {
        // Make agent fault-tolerant using Koog's persistence.
        // The agent will recover from the exact graph node where it crashed
        install(Persistence) {
            // Configure where to store the checkpoints:
            storage = PostgresJdbcPersistenceStorageProvider(dataSource)
        }
    }
}

The persistence feature allows the agent to recover from the exact graph node where it failed and continue execution, which is essential for building reliable services.

History compression

Once you start scaling your AI agents to millions of users and longer-running sessions, managing LLM costs becomes critical. Each step of the agent’s execution, typically a tool call or an LLM request, adds to the message history, and every token has a price. Beyond cost, every model has a context window limit that’s easy to hit when processing large documents, handling tool outputs, or running extended sessions.

You don’t want to silently drop earlier messages when the window fills up. But you also don’t want to pay for irrelevant tokens or risk the model losing important context. Instead of dropping the history, you can replace it with a summary:

private fun AIAgentGraphContextBase.tooManyTokensSpent(): Boolean = 
    llm.prompt.latestTokenUsage > 1000

val graphStrategy = strategy<String, String>("ecommerce_support") {
    ...
    // Compress history node with compression strategy
    val compressLLMHistory by nodeLLMCompressHistory<String>(
        // Substitute every 5 messages with TL;DRs
        strategy = HistoryCompressionStrategy.Chunked(5)
    )
    // Do nothing node for navigation only
    val maybeCompressHistory by nodeDoNothing<String>()

    edge (orderStatusFlow forwardTo maybeCompressHistory)
    edge (maybeCompressHistory forwardTo compressLLMHistory 
        onCondition { tooManyTokensSpent() }
    )
    edge (maybeCompressHistory forwardTo nodeFinish 
        onCondition { !tooManyTokensSpent() }
    )
    edge (compressLLMHistory forwardTo nodeFinish)
}

After adding history compression into the strategy, our updated graph workflow for the  e-commerce agent would look like this:

Check out the full example in Kotlin or Java.

The bottom line

In this article, we saw how you can use Koog and Spring AI together to benefit from Spring AI’s model connections and database integrations, as well as the advanced production-focused orchestration layer from the Koog framework.

If you want to learn more about Koog, its product page is a good place to start, and if you have any questions or feedback, be sure to join the discussion on GitHub.

Finally, don’t forget to join #koog-agentic-framework on the Kotlin Slack (get an invite here).

Sky’s the Limit Hackathon: 180 Projects Connecting Developers and Esports

Earlier this winter, JetBrains and Cloud9 launched Sky’s the Limit, a global hackathon created to bring together two communities that share the same DNA: developers and esports enthusiasts.

The idea was straightforward – developers like solving complex problems, and esports is full of strategy, data, and performance questions. Put those together, and you get plenty of room to build useful things.

The community response was stronger than we expected. Developers, esports fans, analysts, and creators from around the world joined the challenge to build tools, experiences, and insights inspired by competitive gaming.

If you missed the original announcement, you can read more about the hackathon here.

The Numbers Behind the Hackathon

The hackathon brought in 2,293 registrations from developers and esports enthusiasts worldwide. By the submission deadline, participants had delivered an impressive 180 projects, from AI-powered coaching assistants to new fan engagement experiences.

To support participants during the hackathon, we hosted a series of office hours sessions where mentors from JetBrains and Cloud9 answered questions, discussed ideas, and helped teams refine their projects.

Every participant also received free access to the JetBrains All Products Pack and AI Ultimate for two months, which gave them a chance to try JetBrains’ AI coding agent, Junie, while building their projects.

The hackathon was meant to bring software development and esports closer together, giving participants space to experiment with new ideas using real esports data and modern developer tools.

Winning Projects

Below are the winning projects across the four main hackathon categories.

Category 1: A comprehensive assistant coach: C9 StratOS (Daniel Torres)

C9 StratOS is an assistant coaching platform built to help esports teams analyze matches, refine strategies, and improve performance. The project combines advanced data analysis with clear visualizations that help coaches and analysts identify gameplay patterns more quickly.

Category 2: Automated scouting report generator: Spector (David Weatherall) (David Weatherall)

Spector focuses on one of the most time-consuming parts of competitive esports: opponent scouting. The project analyzes gameplay data from VALORANT and League of Legends to generate structured scouting reports. These reports highlight player tendencies, team strategies, and potential weaknesses, giving teams a faster and more systematic way to prepare for upcoming matches.

Category 3: AI drafting assistant/draft predictor: Cloud9 Draft Assistant (James Landry)

Drafting is one of the most strategic phases in competitive esports, where teams must carefully select characters while anticipating their opponent’s choices. The Cloud9 Draft Assistant uses AI to analyze historical match data and model potential draft scenarios. By predicting likely picks, bans, and strategic outcomes, the tool helps teams create stronger draft strategies and prepare more effectively before the match even begins.

Category 4: Event Game: Junie’s Arcade (Ajito Nelson Lucio da Costa)

Junie’s Arcade is a fan experience inspired by JetBrains’ AI coding agent, Junie. The project combines game mechanics with a fun developer-themed narrative and shows how technology and creativity can come together to create engaging experiences at live esports events.

Bonus Awards

In addition to the main competition categories, several projects stood out for their creativity and contribution to the community.

Best blog post: VLML (blog post, Kenneth Adrian Ubales)

VLML stood out for its clear documentation and engaging explanation of the project’s technical ideas. In the blog post, the author walks readers through the development process, covering architecture decisions, challenges, and lessons learned. By sharing his process so openly, he created a useful resource for anyone who wants to understand both the project and the thinking behind it.

Best video: Synapse (Arvind Vivekanandan, Venkat Nallapaneni, Vigneshaditya Nallapaneni)

This submission impressed the judges with a polished and engaging project presentation. The video clearly explains the idea behind the project and shows how the solution works in practice. By combining storytelling, visuals, and technical explanation, the team created a compelling video that highlights both the creativity and the engineering effort behind the project.

Best Junie feedback: Ascend (Yash Moharir)

The author of Ascend provided especially detailed product feedback during the hackathon. His comments covered real development workflows, usability observations, and suggestions for improving the developer experience. Feedback like this helps JetBrains improve its tools.

Celebrating the Winners at GDC

The winners of the main hackathon categories received, among other prizes, an all-expenses-paid trip to the Game Developers Conference (GDC).

During the conference week, the winners joined the JetBrains and Cloud9 teams in San Francisco and experienced GDC firsthand by attending talks, meeting other developers, and connecting with the wider game development community.

One of the highlights of the week was the JetBrains GameDev Night, where we celebrated the hackathon results with developers, partners, and friends of JetBrains. Two of the winning teams presented their projects live on stage, shared the ideas behind their work, and received a warm response from the audience.

Seeing projects that started as hackathon submissions presented live in front of the community was a special moment, showing how quickly creative ideas can turn into real tools and experiences.

The celebration continued at the JetBrains booth during the conference. One of the winning projects, the event mini-game created during the hackathon, was installed at the booth and quickly became a popular attraction. Hundreds of GDC attendees stopped by to try the game, and many replayed it multiple times to improve their scores.

To make things even more exciting, visitors also had the chance to compete against Cloud9 VALORANT pro players Zellsis and V1c, which turned the activity into a mix of gaming, developer curiosity, and esports fun.

Thank You to the Community

The Sky’s the Limit hackathon was a celebration of creativity, collaboration, and innovation across both the developer and esports worlds.

A huge thank you to:

  • Every participant who submitted a project
  • The Cloud9 team for their partnership
  • GRID for providing esports data
  • Everyone who joined our office hours sessions and supported the event
    • The creativity and enthusiasm throughout the hackathon were impressive, and we’re excited to continue exploring what software development, AI, and esports can build together!

      Because when developers and gamers start building together, the sky truly is the limit.

How to Compress SVG Files: Tools, Techniques, Config

How to Compress SVG Files: Tools, Techniques, and Config

SVG files are text. Specifically, they are XML — a tree of elements describing paths, shapes, gradients, filters, and metadata. Unlike raster formats (PNG, JPEG, WebP), compressing SVG has nothing to do with pixel data or color depth. It means stripping redundant XML nodes, simplifying path definitions, removing editor junk, and delivering the cleaned result with transfer-level compression.

A typical icon exported from Illustrator v29 weighs 45KB. After optimization, the same icon renders identically at 8KB. That is an 82% reduction with zero visual change — and the file is still a fully editable vector.

This guide covers every layer of SVG compression: automated tools, manual cleanup techniques, server-side delivery, and Illustrator export settings that prevent bloat in the first place.

<p>Working with images?</p>
<p>Pixotter compresses raster images (PNG, JPEG, WebP) in-browser with no upload and no signup. Drop, compress, download.</p>

Compress Images →

Why SVG Compression Is Different

Raster compression reduces pixel data — fewer colors, approximate values, smaller grids. SVG compression reduces text. The file is XML source code, and like any source code, it accumulates cruft:

  • Editor metadata. Illustrator, Figma, and Inkscape each embed their own XML namespaces, layer names, generator comments, and internal IDs. None of this affects rendering.
  • Redundant attributes. fill="#000000" on an element that inherits fill="#000000" from its parent. style="display:inline" on an element that is inline by default.
  • Verbose path data. Path commands with 8 decimal places when 2 would produce identical on-screen output. Absolute coordinates where relative deltas would be shorter. Curves that approximate straight lines.
  • Invisible elements. Hidden layers, zero-opacity groups, elements fully clipped outside the viewBox.
  • Embedded raster data. Some export flows embed PNG or JPEG thumbnails as base64 data URIs inside the SVG. A 200-byte icon suddenly weighs 40KB.

Removing this waste does not change what the browser draws. The rendering engine ignores metadata, recomputes inherited styles, and rounds sub-pixel coordinates anyway. You are deleting what the browser already discards.

SVG Compression Techniques Compared

Technique Tool / Method Typical Savings Quality Risk Automation Best For
Automated optimization SVGO v3.3.2 40–80% None (safe defaults) CLI, Node.js API All SVGs — the baseline step
Manual cleanup Text editor 10–30% (on top of SVGO) None if careful Manual Complex SVGs where SVGO misses domain-specific waste
Transfer compression gzip / brotli 60–75% additional None Server config All SVGs served over HTTP
Export settings Illustrator v29 Prevents 50–90% of bloat None Per-export Designers producing SVGs
Online tools SVGOMG, Pixotter 40–70% Depends on settings Browser-based Quick one-off jobs

These techniques stack. SVGO strips XML waste, manual cleanup catches what SVGO cannot, and gzip/brotli compresses the cleaned text for transfer. An icon that starts at 45KB → 8KB after SVGO → 2.5KB over brotli.

SVGO: The Standard SVG Optimizer

SVGO (SVG Optimizer, MIT license) is the de facto tool for SVG compression. Most online SVG optimizers — including SVGOMG — are wrappers around SVGO. If you only learn one tool from this guide, this is the one.

Install and Basic Usage

SVGO v3.3.2 requires Node.js v22 or later.

# Install SVGO v3.3.2 globally
npm install -g svgo@3.3.2

# Compress a single SVG
svgo input.svg -o output.svg

# Compress in-place (overwrites the original)
svgo input.svg

# Compress all SVGs in a directory
svgo -f ./icons/ -o ./icons-optimized/

# Compress recursively
svgo -f ./assets/ -o ./assets-optimized/ -r

With default settings, SVGO removes metadata, strips comments, merges paths, collapses useless groups, converts colors to shortest form, and optimizes path data. Default behavior covers 90% of cases.

Custom Configuration

For more control, create an svgo.config.mjs file in your project root:

// svgo.config.mjs — SVGO v3.3.2 configuration
export default {
  multipass: true, // Run multiple passes until no more savings
  plugins: [
    {
      name: 'preset-default',
      params: {
        overrides: {
          // Keep viewBox (needed for responsive scaling)
          removeViewBox: false,

          // Reduce path precision from default 3 to 2
          // Saves bytes on complex paths, safe for most icons
          cleanupNumericValues: {
            floatPrecision: 2,
          },

          // Keep IDs used by CSS or JavaScript
          // Set to false if your SVGs have meaningful IDs
          cleanupIds: {
            remove: true,
            minify: true,
          },
        },
      },
    },

    // Remove Illustrator/Inkscape-specific elements
    'removeEditorsNSData',

    // Sort attributes for better gzip compression
    // (repeated patterns compress better when ordered consistently)
    'sortAttrs',

    // Convert inline styles to attributes where shorter
    'convertStyleToAttrs',
  ],
};

Run SVGO with the config:

svgo --config svgo.config.mjs input.svg -o output.svg

Plugins That Matter

SVGO ships with 30+ plugins. These are the ones that produce the largest savings:

Plugin What It Does Default On?
removeDoctype Strips <!DOCTYPE> declaration Yes
removeComments Strips XML comments Yes
removeMetadata Strips <metadata> elements Yes
removeEditorsNSData Strips Illustrator/Inkscape namespace data Yes
cleanupNumericValues Rounds numbers to floatPrecision decimals Yes (3 decimals)
convertPathData Converts absolute to relative coords, removes redundant commands Yes
mergePaths Merges adjacent <path> elements with identical styles Yes
collapseGroups Removes <g> wrappers that have no attributes Yes
removeViewBox Removes the viewBox attribute Yes (override this to false)
sortAttrs Orders attributes alphabetically for gzip efficiency No

One override you should always apply: disable removeViewBox. The viewBox attribute is essential for responsive SVGs that scale to their container. Without it, your SVG has a fixed pixel size.

Build Pipeline Integration

For projects using a bundler, add SVGO to your build step:

# package.json script — compress all SVGs before build
# Requires: npm install -D svgo@3.3.2
{
  "scripts": {
    "optimize:svg": "svgo -f ./src/assets/icons -o ./src/assets/icons -r",
    "build": "npm run optimize:svg && vite build"
  }
}

For Vite projects specifically, vite-plugin-svgo (MIT license) runs SVGO on SVG imports at build time.

Manual SVG Optimization Techniques

SVGO handles the mechanical work. Manual optimization handles the domain-specific work — things that require understanding what the SVG represents to know what can be simplified.

Remove Embedded Raster Data

Design tools sometimes embed raster previews or textures as base64 data URIs. Look for <image href="data:image/png;base64,..." elements. A single embedded PNG can add 30–100KB to an icon that should be 2KB. Remove these elements entirely, or replace the raster texture with an SVG pattern.

Simplify Path Coordinates

SVGO reduces decimal precision, but you can go further for simple shapes. An icon path with coordinates like M 12.003906 4.003906 L 12.003906 20.003906 is storing precision the screen cannot display. At icon sizes (16–48px), two decimal places are more than enough. At large sizes, three decimals cover every display density up to 8K.

Replace Inline Styles with CSS

If an SVG has many elements sharing the same style, a single CSS rule is shorter than repeating inline style attributes:

<!-- Before: 340 bytes of repeated inline styles -->
<rect style="fill:#3b82f6;stroke:#1e40af;stroke-width:2" ... />
<rect style="fill:#3b82f6;stroke:#1e40af;stroke-width:2" ... />
<rect style="fill:#3b82f6;stroke:#1e40af;stroke-width:2" ... />

<!-- After: 180 bytes with a shared class -->
<style>.box{fill:#3b82f6;stroke:#1e40af;stroke-width:2}</style>
<rect class="box" ... />
<rect class="box" ... />
<rect class="box" ... />

This saves bytes and makes the SVG easier to theme with external CSS.

Merge Overlapping Paths

Two <path> elements with identical fill and stroke can often be combined into a single <path> with a compound path definition (multiple M commands in one d attribute). SVGO’s mergePaths plugin does this automatically for adjacent same-style paths, but it cannot merge paths separated by other elements. Manual reordering + merge can squeeze out another 5–15%.

Remove Hidden Elements

Check for:

  • Elements with display="none" or visibility="hidden"
  • Elements with opacity="0"
  • Groups that contain nothing after cleanup
  • Elements positioned entirely outside the viewBox
  • Clip paths that clip everything away

Design tools often leave hidden layers in the export. They contribute zero pixels and full bytes.

Server-Side Compression: gzip and Brotli

SVG is text. Text compresses exceptionally well with general-purpose algorithms. Serving SVG without transfer compression is leaving 60–75% of the bandwidth savings on the table.

Both gzip and Brotli operate at the HTTP layer — the SVG file stays the same on disk, but the server compresses it during transfer, and the browser decompresses it transparently. Every modern browser supports both.

Nginx Configuration

# /etc/nginx/nginx.conf — enable compression for SVG
gzip on;
gzip_types image/svg+xml;
gzip_min_length 256;
gzip_comp_level 6;

# Brotli (requires ngx_brotli module)
brotli on;
brotli_types image/svg+xml;
brotli_min_length 256;
brotli_comp_level 6;

Apache Configuration

# .htaccess or httpd.conf
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>

# Brotli (requires mod_brotli, Apache 2.4.26+)
<IfModule mod_brotli.c>
  AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
</IfModule>

Pre-Compression for Static Sites

If your SVGs are static assets, pre-compress them at build time for maximum compression without runtime CPU cost:

# Pre-compress with gzip (maximum compression)
gzip -k -9 icon.svg
# Creates icon.svg.gz alongside icon.svg

# Pre-compress with brotli (maximum compression)
brotli -k -q 11 icon.svg
# Creates icon.svg.br alongside icon.svg

Configure Nginx to serve the pre-compressed version when the browser supports it:

gzip_static on;
brotli_static on;

Transfer Compression Savings

File Raw After SVGO Over gzip Over Brotli
Icon (simple, 24px) 2.1 KB 0.8 KB 0.4 KB 0.35 KB
Logo (moderate complexity) 18 KB 6 KB 2.1 KB 1.8 KB
Illustration (complex) 120 KB 45 KB 14 KB 11 KB
Icon set (50 icons, combined) 95 KB 32 KB 9 KB 7 KB

Brotli consistently beats gzip by 15–20% on SVG content. If your server supports both (and most CDNs do), prefer brotli with gzip as fallback.

Illustrator v29 SVG Export Settings

The best compression happens before you even run SVGO — at export time. Illustrator v29’s File > Export > Export As > SVG dialog has settings that control how much junk ends up in the file.

Setting Recommended Value Why
Styling Presentation Attributes Shortest output. Inline styles and Internal CSS are verbose.
Font Convert to Outlines Eliminates font embedding. If the SVG uses text, outline it.
Images Link (not Embed) Prevents base64 raster data from bloating the file. Remove linked images if not needed.
Object IDs Layer Names or Minimal “Layer Names” keeps meaningful IDs for CSS/JS targeting. “Minimal” is smallest.
Decimal 2 Matches SVGO’s recommended floatPrecision. Lower = smaller, but 1 can distort curves.
Minify Yes Strips whitespace and newlines.
Responsive Yes Adds viewBox and removes fixed width/height. Essential for responsive SVGs.

With these settings, an Illustrator export is already 40–60% smaller than the default export. Running SVGO on top of that still removes Illustrator-specific namespace data and further optimizes paths, but you start from a much cleaner baseline.

Figma note: Figma’s SVG export is cleaner than Illustrator’s by default — no namespace junk, no embedded rasters. SVGO still helps (path optimization, attribute sorting), but the starting point is smaller.

Before and After: Real Compression Results

These numbers come from compressing a set of UI icons originally exported from Illustrator v29 with default settings:

Asset Original After Illustrator Settings After SVGO v3.3.2 Over Brotli Total Reduction
Shopping cart icon 12 KB 6.2 KB 2.1 KB 0.9 KB 92%
Navigation hamburger 4.8 KB 2.4 KB 0.6 KB 0.3 KB 94%
User avatar placeholder 28 KB 14 KB 5.2 KB 1.8 KB 94%
Company logo (moderate) 45 KB 22 KB 8 KB 2.8 KB 94%
Detailed illustration 180 KB 95 KB 52 KB 16 KB 91%

The pattern: simple icons see 90–95% total reduction. Complex illustrations see 85–92%. The biggest single gain comes from SVGO, but each layer compounds.

For an icon set of 50 files totaling 450KB, the optimized + brotli-compressed set totals under 30KB. That is the difference between a noticeable load delay and instant rendering.

When NOT to Compress SVG

SVG compression is not always safe. Two scenarios require caution:

Complex Illustrations with Precise Curves

Path simplification works by removing control points and reducing decimal precision. For simple icons, this is invisible. For detailed illustrations — botanical drawings, architectural plans, typographic art — reducing floatPrecision below 3 can visibly distort curves. Test at the actual display size before committing.

If you see curve distortion after SVGO, increase floatPrecision to 3 or 4 in the config and disable convertPathData for that specific file:

// svgo.config.mjs — conservative settings for detailed SVGs
export default {
  plugins: [
    {
      name: 'preset-default',
      params: {
        overrides: {
          cleanupNumericValues: { floatPrecision: 4 },
          convertPathData: false, // Preserve original path commands
          removeViewBox: false,
        },
      },
    },
  ],
};

SVGs with JavaScript or Animation Dependencies

SVGO’s cleanupIds plugin minifies element IDs. If your SVG has JavaScript that references elements by ID (document.getElementById('my-icon-path')) or CSS animations targeting specific IDs (#my-icon-path { animation: spin 2s; }), minified IDs break those references. Either disable cleanupIds or use class-based selectors instead.

Similarly, collapseGroups removes structurally insignificant <g> elements. If your JavaScript traverses the SVG DOM expecting specific group nesting, disable this plugin.

How Pixotter Handles SVG

Pixotter is built for raster image optimization — compressing PNGs, JPEGs, and WebP files in-browser via WebAssembly. SVG is a vector format and requires a fundamentally different optimization pipeline (XML manipulation rather than pixel-level compression).

For raster workflows that involve SVG, Pixotter fits into the pipeline at the conversion step. If you need to convert SVG to PNG for a context that requires raster output, convert SVG to JPG for email compatibility, or convert SVG to PDF for print-ready files, Pixotter handles the raster side — compressing the converted output to the smallest file size with zero quality loss.

For SVG-specific optimization, use SVGO as described above. For the raster images in your project, Pixotter’s compress tool handles everything client-side with no upload and no signup.

Frequently Asked Questions

Does compressing SVG reduce visual quality?

Not with default SVGO settings. The default plugins remove metadata, comments, editor data, and redundant attributes — none of which affect rendering. Path optimization with floatPrecision: 3 (the default) produces sub-pixel differences invisible on any display. Reducing precision to 1 can distort complex curves, so test at the target display size if you go below 2.

What is the best tool to compress SVG?

SVGO v3.3.2 is the industry standard. It is open source (MIT license), actively maintained, used by nearly every SVG optimization tool, and runs on any system with Node.js v22. For a browser-based interface, SVGOMG is a wrapper around SVGO with a visual diff preview. For quick raster image compression, Pixotter handles PNG, JPEG, and WebP.

Can I compress SVG without installing anything?

Yes. SVGOMG runs entirely in the browser — paste your SVG or upload the file, toggle plugins, and download the result. No install, no signup. For raster image compression with the same zero-install approach, Pixotter works the same way.

Should I gzip SVG files?

Absolutely. SVG is XML text and compresses 60–75% under gzip, 65–80% under brotli. Always enable transfer compression for SVG on your web server. This stacks with SVGO optimization — a file cleaned by SVGO and served with brotli can be 95% smaller than the original over the wire.

Is SVG or PNG smaller for icons?

SVG is almost always smaller for simple icons. A 24px icon is typically 0.5–2KB as SVG versus 3–8KB as PNG (including 2x retina version). SVG also scales to any size from a single file, eliminating the need for multiple resolution variants. See the SVG vs PNG comparison for a full breakdown, or the best image format for web guide for choosing across all formats.

How much can I compress an SVG file?

It depends on the source. Design tool exports with default settings typically compress 70–90% after SVGO + server-side brotli. Hand-coded SVGs or already-optimized files may only compress 10–20%. Icons see the highest ratios; complex illustrations see the lowest. The transfer compression table above shows real numbers by asset type.

Does SVG compression affect accessibility?

SVGO preserves <title> and <desc> elements by default (the removeTitle and removeDesc plugins are off). These elements provide accessible names for screen readers. If you enable those plugins manually, you remove the accessible labels. Keep them disabled unless you provide accessibility via aria-label on the SVG element instead.

I Spent 40 Hours Building AI Prompts for Competitive Intelligence — Here’s What Actually Works

I Spent 40 Hours Building AI Prompts for Competitive Intelligence — Here’s What Actually Works

I run competitive intelligence for SaaS founders. Every week I produce detailed reports on competitor pricing, positioning, customer sentiment, and market moves.

For the first year I did it manually. Hours on G2, hours on LinkedIn, hours reading job listings to reverse-engineer strategy. It was brutal.

Then I rebuilt the entire workflow using Claude and ChatGPT. What used to take 6 hours now takes 20 minutes. The output is better — more structured, more actionable, covers angles I used to miss.

I’m sharing the exact prompts I use. No fluff. Just the prompts, what they produce, and when to use each one.

Why Most People Fail at Competitive Intelligence

They approach it wrong. They ask “what does Competitor X do?” instead of asking the right questions:

  • What do their unhappy customers say? (That’s where your positioning lives)
  • What are they hiring for? (That reveals their next 6 months of strategy)
  • What does their pricing architecture signal? (That tells you who they’re actually targeting)
  • What content gaps exist? (That’s free SEO traffic waiting for you)

Generic research gives you generic insights. Specific questions give you edges.

The 10 Prompts (In Order of Impact)

1. The Master Report Prompt

Use this when you need a comprehensive view of a competitor. Takes 5 minutes, outputs a 15-page analysis.

You are a senior competitive intelligence analyst. Produce a complete report on [COMPETITOR].

Include:
- Company overview (funding, team size, revenue estimates)
- Product strengths and weaknesses (be specific, not generic)
- Full pricing breakdown with hidden costs
- Customer sentiment analysis using actual review language from G2/Reddit
- Marketing and messaging analysis
- Competitive threat assessment (1-10 with reasoning)
- Top 5 specific opportunities to win against them

My company: [YOUR COMPANY]
My target customer: [YOUR ICP]

I ran this on Notion last week. Here’s the full output — 15 pages, actual pricing data, real customer quotes, positioning map. Free to read.

2. Review Mining — The Most Underused Technique

Your competitors’ reviews are a goldmine. Their unhappy customers describe exactly what you should build and exactly what messaging you should use.

Analyze [COMPETITOR] reviews from G2, Reddit, and Trustpilot.

Produce:
1. Top 5 complaints — with exact language patterns customers use
2. Top 5 praise points — what keeps customers loyal
3. Churn triggers — specific events that cause customers to leave
4. Unmet needs — what customers wish the product did
5. LANGUAGE GOLD — exact phrases customers use to describe their pain (use these as your ad copy and email subject lines)
6. Who is actually buying — job title, company size, use case

The “language gold” section alone is worth running this prompt for. When you use the exact words your prospects use to describe their problem, conversion rates spike.

3. Pricing Strategy Decoder

Pricing pages are deliberately confusing. This prompt cuts through it.

Analyze [COMPETITOR]'s pricing strategy.

Tell me:
1. What's the upgrade trigger at each tier? (What forces someone to pay more?)
2. What's the "hero" plan they want everyone to buy?
3. What plan are they hoping you DON'T buy?
4. What's the real annual cost for a 20-person company?
5. Where do reviews mention "too expensive" or "hidden costs"?
6. How should I price against them — cheaper, more expensive, or differently structured?

After running this on 5 competitors, I found that every single one had at least one tier designed to look attractive but actually deliver poor value. That’s where you position.

4. Job Listing Intelligence

This one surprises people. Job listings are one of the best signals of where a company is heading.

Analyze [COMPETITOR]'s recent job postings.

Tell me:
1. Strategic priorities — based on what roles they're hiring, what are their top 3 bets right now?
2. Product direction — what are the technical/product hires signaling?
3. Go-to-market shifts — what do sales/marketing hires reveal?
4. What are they NOT hiring for? (What are they keeping small?)
5. Hiring acceleration or slowdown — growth or trouble?
6. What will they likely launch or change in the next 6 months?

Their jobs page: [URL]

I ran this on a Series B SaaS last month. They were hiring 3 enterprise AEs and a Head of Compliance. Six weeks later they announced an enterprise tier with SOC2. Knowing that in advance would have been worth thousands.

5. Rapid Sales Brief (15 Minutes Before a Call)

Give me a rapid competitive brief on [COMPETITOR] for a sales call in 30 minutes.

- What they do (1 sentence)
- Their best customer (1 sentence)
- Their 3 biggest strengths
- Their 3 biggest weaknesses
- Price range
- 3 objections a prospect might raise comparing us
- 3 responses to those objections
- The ONE thing I should never say when they bring up [COMPETITOR]

Be direct. No padding.

This one I run before every sales call. Takes 3 minutes to run, saves 20 minutes of scrambling.

The Full Pack

I packaged all 10 prompts (including Feature Gap Analysis, Content Gap Analysis, Win/Loss Framework, Market Trend Briefing, and more) into a downloadable pack.

It also includes the complete Notion competitive analysis I mentioned above — 15 pages showing exactly what these prompts produce when you run them properly.

AI Competitive Intelligence Starter Pack — $29

If you want us to run this for your business every week instead: Claw Intelligence — from $399/month.

The Meta-Lesson

The prompts are just tools. The real skill is knowing which questions to ask and what to do with the answers.

Most competitive intelligence fails because people collect information without asking “what decision does this help me make?”

Every prompt above is designed around a specific decision:

  • Pricing prompt → how should I price?
  • Review mining → what messaging should I use?
  • Job listing → what should I build next?
  • Sales brief → how do I win this deal?

Run them with that framing and you’ll get outputs you can actually act on.

Questions? Drop them below. I read every comment.