← Writing
K2 / Nintex 6 min read

Why Your K2 Forms Are Slow: A Field Guide

A hands-on guide to diagnosing slow K2 SmartForms: the usual suspects, how to spot each one, and how to measure instead of guess.

The promise of low-code is that you build fast. Nobody warns you that you can ship something slow just as fast. I learned this on a form that worked beautifully in the demo and crawled in production.

The complaint came in as one line: “the approval screen takes forever to open.” Forever turned out to be about eleven seconds. Eleven seconds of a spinner on a form that, in my dev environment, opened instantly. That gap between dev and production is the whole story, so let me debug it the way I actually did, in the open.

Measure before you guess

The first rule is that you do not get to have an opinion yet. Open the browser devtools, go to the Network tab, clear it, and load the form. Then read what is actually there.

One thing to keep straight: on classic SmartForms the runtime batches many server-side calls into a few AJAX posts, so the browser Network tab often shows one request, not one per SmartObject. The total round-trip time is reliable there. To see individual SmartObject executions you usually need server-side runtime logging or the SmartObject call trace. The browser tab is more directly useful on newer SSR or API-driven form runtimes, where a slow 1200ms call and a chain of forty 40ms calls feel identical to the user but are completely different problems, and the waterfall tells you which one you have.

So I turned on runtime logging. The form was firing a per-row lookup for every pending item, plus a handful of reference reads, and the whole pile ran while the user stared at a spinner.

Time the SmartObject, not just the form

Before you blame the form for everything, separate the two suspects. A slow form and a slow service look identical to the user and need opposite fixes. This one split saves you hours, because it stops you tuning a view that was never the bottleneck.

Execute the SmartObject in isolation (the SmartObject Service Tester on a dev box, or via the management and SQL tooling) and time it, by stopwatch or, better, by server-side logging or SQL Profiler on the underlying query. Note the Tester is a server-side utility you will not have on a locked-down production server. If a single call takes two seconds, the form is innocent and you have a data problem: depending on the broker, that is a missing index or unfiltered query for a SQL-backed SmartObject, or a slow remote system (REST, SharePoint, AD, custom broker) where tuning happens on the far side.

In my case the list SmartObject was fine on its own, around 200ms. The form was multiplying a fast call. Good to know, because it tells you to fix structure, not the database.

The N plus one is the most common cause

K2 does not fan out per row on its own. An ordinary List View bound to one List method calls it once and binds the result. The N plus one comes from specific designs: a rule that loops the list editor calling a Load method per row, a per-row expression that resolves a label through a separate SmartObject, or a repeating section with an explicit per-row data binding. That was my form: each pending row resolved a department through its own call.

The fix is to stop asking N times. Extend the SmartObject behind the list so it returns the department in the same result set, ideally as a join at the source. One call, all rows, department included. If you cannot change the SmartObject, load the lookup once into a form-level data control and resolve display values against that single in-memory result instead of calling out per row.

Filter at the source, not in the browser

The next thing logging showed me was a lookup control pulling an entire reference table, several thousand rows, to display the four that matched the current user. Compare the row count in the response to the row count on screen. If the source returns 4,000 and the user sees 8, the filter is in the wrong place.

Filter as early as possible. A SmartObject method input that maps to a WHERE clause is best, because it filters at the database. A list filter applied in the SmartForms layer still helps the browser, but the broker may already have pulled the full set, so push the filter into the service or query where you can.

Heavy controls build heavy DOM

One view rendered a List View with twelve columns and no row cap. Most of those columns were never read by an approver. The data transfer is not even the slow part. The rendering is. The tell is the gap between the network call finishing and the view becoming interactive.

Cut the columns to what the user needs. Where the control and SmartObject support server-side paging, push the page boundary to the source so it returns one page; be aware some List View paging happens client-side over a result set already returned, which does not reduce the round trip, so verify where it actually happens. An editable List View bound to an unfiltered, unpaged source is one of the heavier things you can put on a form, because it carries edit state per row on top of the data.

Rules that fire on everything

When I opened the rules, the pattern was familiar. Work was wired to “When the View executed Initialize” and to control-change events, re-running lookups that only needed to run once. Make the work conditional, and defer what does not need to happen on load. A rule should only do its work when its input actually changed.

By default many embedded views initialize on form load, and an initialize rule that pulls data is a data call, so several views can fetch before the user touches anything. Whether a view on an unopened tab initializes depends on the tab and view configuration, which is exactly the lever you use to defer it.

Change one thing, then re-measure

I changed exactly one thing at a time and re-measured after each. The batched SmartObject. Then the filter. Then the columns. Then the rules. Batch the calls, filter at the source, make rules conditional, flatten the nesting. Guessing at two fixes at once means you never learn which one worked. Eleven seconds came down to under two, and I could point at each second I removed.

A slow form is a structure problem wearing a costume

None of these fixes were clever. They were about asking for less, asking once, and asking only for what the user will see. That is the same point I made in Low-Code Is Still Code: a workflow still has state, a form still has dependencies, and a SmartObject still talks to a system that can be slow. The drag-and-drop surface hides the cost, it does not remove it. Take the costume off and deal with what is underneath.