Writing a fast data page in Ring — five rules and one payload shape

For app programmers August 2026 · from the RingScript project

These rules come from building a real application — the savings-circle register that holds 20,000 records in the browser — and every one of them was learned by getting it wrong first. None is folklore: each has a measurement behind it, and together they took the app from sluggish to instant.

1Index your loops — for in copies every item

Ring's for x in aList hands you a copy of each item. On a list of records, that is a copy of every record, every pass. Index instead, and read through the list directly:

copies each record into x
for x in aDeposits
    nSum += x[4]
next
reads in place
nCount = len(aDeposits)
for i = 1 to nCount
    nSum += aDeposits[i][4]
next

Note the second half of the rule: len() was hoisted out of the loop header, because Ring re-evaluates it on every iteration.

2Assignment copies too — mutate through the list

The same value semantics mean o = aMembers[k] gives you a copy; writing to it updates nothing anyone can see. Mutate through the list itself:

writes vanish into a copy
oM = aMembers[k]
oM[2] = oM[2] + nAmount     # the list never changes
writes land
aMembers[k][2] = aMembers[k][2] + nAmount

3Helpers that take big values pay for them

Arguments copy as well. A tidy FieldOf(aRecord, "amount") helper duplicates the whole record on every call — four fields read means four copies of the record. In hot loops, read fields inline in one pass over the record instead of calling out per field.

4Append with +

aList + item is the operator form of append, and it adds a sublist as one item — so building rows like aMembers + [cName, 0, 0] does what you expect. It measured consistently faster than add() in our loops, and reads better too.

5Functions called from the page take exactly one parameter

ring.call("MyFunc", arg) always passes one argument — so func MyFunc p is the shape, even when you ignore p. A zero-parameter function simply errors, and the message will not tell you why. And one naming trap: do not shadow a builtin — func Load collides with Ring's own load and silently fails to define.

+The payload shape: ask your API for rows, not objects

The single biggest lever is not in a loop at all. A JSON object decodes in Ring into a pair-list — one list for the record, one list per field, two items per field. A JSON array row is one list and one item per field. Same data, very different cost:

20,000 records, same dataPayloadLoadHeap
[{"id":1,"member":"m03",…}]1.31 MB1358 ms+89 MB
[[1,"m03",3,250,"ACTIVE"]]0.55 MB71 ms+23 MB

Fifteen times faster to load, a quarter of the memory. Most of that saved second was never parsing — it was first-touching 89 MB of heap the page immediately threw away. If you control the API, send rows; if you might receive either, the register app shows how to accept both.

All five rules — plus the two the runtime enforces for you — are written at the top of the register app's source, where the next reader meets them before the bug does. For what the finished page feels like at 20,000 records, see Working with data.