From e7ba6071a5b3e80d56f4df587d7e3cd0e2091e07 Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Fri, 16 Oct 2026 07:44:57 +0800 Subject: [PATCH] docs(app-skills): add performance guidance to app development skill (#10937) Cover frequently called endpoints, read-only transactions, list response size, client refresh, bounded concurrency, background work, leases and realistic measurement. --- .changeset/app-skills-performance-guidance.md | 5 +++++ .../skills/nocobase-app-development/SKILL.md | 2 +- .../references/frontend/references/api.md | 5 +++++ .../references/server-routes.md | 12 +++++++++++- .../references/services-and-jobs.md | 10 ++++++++++ .../nocobase-app-development/references/testing.md | 2 ++ 6 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .changeset/app-skills-performance-guidance.md diff --git a/.changeset/app-skills-performance-guidance.md b/.changeset/app-skills-performance-guidance.md new file mode 100644 index 00000000000..3b3e9dd218a --- /dev/null +++ b/.changeset/app-skills-performance-guidance.md @@ -0,0 +1,5 @@ +--- +'@nocobase/app-skills': patch +--- + +Add performance guidance to the application development Skill: frequently called endpoints, transactions for reads, list response size, client refresh and subscriptions, bounded concurrency, background work, timeouts and leases, and measuring on a realistic database and data volume. diff --git a/packages/app/app-skills/skills/nocobase-app-development/SKILL.md b/packages/app/app-skills/skills/nocobase-app-development/SKILL.md index 385badb67c0..c1f7632e16d 100644 --- a/packages/app/app-skills/skills/nocobase-app-development/SKILL.md +++ b/packages/app/app-skills/skills/nocobase-app-development/SKILL.md @@ -84,7 +84,7 @@ Read the page for the task in front of you. Do not read all of them. | Anything under `client/`: pages, routes, components, styling, forms, API calls, copy, frontend tests | [frontend workflow](references/frontend/ui-workflow.md), which routes to the [frontend handbook](references/frontend/frontend-dev.md), the theme reference or the worked example | | Add an API endpoint, a webhook, or a callback; authenticate and authorize it | [server routes](references/server-routes.md) | | Name an endpoint, shape its response or errors, validate its input, paginate a list, document it in the API document | [HTTP API design](references/http-api.md) | -| A list, dashboard or chart endpoint, or keeping one live: read with a fixed number of queries, refresh only what changed | [server routes](references/server-routes.md#lists-and-statistics-a-fixed-number-of-queries) and [client API requests](references/frontend/references/api.md#keeping-lists-and-charts-live) | +| A list, chart or polled endpoint, or keeping one live: read it with a fixed number of queries, refresh only what changed | [server routes](references/server-routes.md#lists-and-statistics-a-fixed-number-of-queries) and [client API requests](references/frontend/references/api.md#keeping-lists-and-charts-live) | | Query or write data, resolve the database, work with transactions | [database and data access](references/database-and-data.md) | | Create a table, alter a column, add an index, write required initial data | [migrations and seeds](references/migrations.md) | | Switch the database, register a dialect, add a second connection | [database connections](references/database-connections.md) | diff --git a/packages/app/app-skills/skills/nocobase-app-development/references/frontend/references/api.md b/packages/app/app-skills/skills/nocobase-app-development/references/frontend/references/api.md index 7935555f471..9e395e106d6 100644 --- a/packages/app/app-skills/skills/nocobase-app-development/references/frontend/references/api.md +++ b/packages/app/app-skills/skills/nocobase-app-development/references/frontend/references/api.md @@ -307,6 +307,11 @@ When realtime events refresh what a page shows: - Recompute a chart's figures at most every few seconds while it is visible, and not at all while the page is hidden; or update only the figure the event affects. - Read nothing for a page that is not shown; read it when it is shown again. - A component that needs one small fact reads the small endpoint that answers it, not the list that happens to contain it. +- Keep realtime subscriptions in one place, such as the page or a hook it owns, rather than one per row or card, and subscribe only to the topics the page shows. +- When a refresh is triggered while a request for the same data is still in flight, usually let it finish and read once more afterwards rather than aborting it and sending it again; repeated aborts under a stream of events can mean no request ever completes. Aborting is still right when the parameters change or the component unmounts, as above. +- Skip fetching while the page is hidden (`document.visibilityState`), and refresh once when it becomes visible again. +- Keep request keys stable: build them from the parameters that decide the data, not from values that change on every render or tick, such as the current time, or each change starts a new request. +- Be careful with data loaded by components mounted on every page, such as navigation or a header badge: they run on every page and in every open tab, so they usually read a small fact rather than a list. The server side of these reads is in [server routes](../../server-routes.md#lists-and-statistics-a-fixed-number-of-queries). diff --git a/packages/app/app-skills/skills/nocobase-app-development/references/server-routes.md b/packages/app/app-skills/skills/nocobase-app-development/references/server-routes.md index ed8ea8e0844..0b41dc44f0d 100644 --- a/packages/app/app-skills/skills/nocobase-app-development/references/server-routes.md +++ b/packages/app/app-skills/skills/nocobase-app-development/references/server-routes.md @@ -238,7 +238,8 @@ Do not write a helper that mutates a router passed in by its caller. Returning a A route that answers many rows or figures reads them with a fixed number of queries, whatever the row count. The database API is in the `nocobase-db` Skill. - Read each kind of data once for all rows: one query with `IN (...)` over the row ids, then join the results in memory. Never call a per-item function or Repository read inside a loop or a `Promise.all` over rows. -- A list row carries only what the row shows. Expensive per-item work — diagnostics, "why is this waiting", full histories — belongs to the item's own detail endpoint. +- When per-item database work genuinely cannot be batched, bound its concurrency to a small number rather than starting one query per item at once: an unbounded `Promise.all` can exhaust the connection pool and stall every other request, not just this one. +- A list row carries only what the row shows. Expensive per-item work — diagnostics, "why is this waiting", full histories — belongs to the item's own detail endpoint, which the client usually reads when a detail view opens. When you add a field to a list response, check how much it adds to the response size across a full page of rows. - A component that needs one small fact, such as whether the viewer follows an item, gets a small endpoint answering that fact rather than reading a whole list or overview. - Aggregate in the database: `COUNT`, `SUM`, `GROUP BY` through the Repository's `count`/`groupBy` or the query builder. Never load rows to count or sum them in JavaScript. - One chart is one request, so charts load, fail, refresh and cache on their own. Each chart's request computes its figures with a fixed number of aggregate queries: one `GROUP BY` day, not a query per day or per item. @@ -248,6 +249,14 @@ A route that answers many rows or figures reads them with a fixed number of quer The browser side — refreshing such a list or chart from realtime events — is in [client API requests](frontend/references/api.md#keeping-lists-and-charts-live). +## Frequently called endpoints + +An endpoint called on a timer — polling, a heartbeat, a status badge every open page reads — costs its per-call price times its call frequency across every client, so a few milliseconds per call can add up to a large share of the database's time. Estimate both before writing one. + +- Usually keep it read-only, without writes or a transaction. Work that has to write, such as recording last-seen times or cleaning up, can often move to a less frequent place: a scheduled job (see [services and jobs](services-and-jobs.md#work-that-runs-on-a-schedule)), or a write only when the value actually changed. +- Read-only code usually needs no transaction, here or anywhere else. A transaction holds a connection for its whole duration and can make reads cost more; use one only for writes that must succeed or fail together. +- Prefer answering a small fact over a list or overview, as the section above describes, and consider caching it briefly when many clients ask the same question. + ## Keep the layers apart Routes handle HTTP: parsing input, checking permission, shaping the response and status. Domain logic goes in a service under `server/providers/` — see [services and jobs](services-and-jobs.md). A service should not read a Hono context or decide status codes. @@ -281,3 +290,4 @@ In React components and custom Hooks, use `useApiClient()` from `@nocobase/app-c - Every `/api` route is declared: after `app.start()`, `findUndeclaredApiRoutes(app)` is empty, and the route appears in `GET /api/swagger` with its `operationId`. See the Verify section of [HTTP API design](http-api.md#verify). - A public route rejects missing and invalid signatures, and handles duplicate delivery. - A list or statistics route makes the same number of queries for ten rows as for a thousand. +- A frequently called route stays read-only and outside a transaction, or its writes have a reason to be there. diff --git a/packages/app/app-skills/skills/nocobase-app-development/references/services-and-jobs.md b/packages/app/app-skills/skills/nocobase-app-development/references/services-and-jobs.md index a038ee9c6ce..61983428394 100644 --- a/packages/app/app-skills/skills/nocobase-app-development/references/services-and-jobs.md +++ b/packages/app/app-skills/skills/nocobase-app-development/references/services-and-jobs.md @@ -222,6 +222,16 @@ await app.container See the `@nocobase/queue` README for the full contract. +## Keep background work light + +Background and maintenance work shares the database and its connection pool with the requests users are waiting on, so it should usually not block them. + +- Maintenance usually should not pause the main flow. Prefer work that runs beside it over a step that holds requests or other jobs until it finishes. +- Time-box long work or do it in batches: process a bounded number of rows per run or per transaction, and continue in the next run, rather than one long transaction or one query over a whole table. +- When per-item work queries the database, batch it or bound its concurrency, as [server routes](server-routes.md#lists-and-statistics-a-fixed-number-of-queries) describes for lists. +- When the inputs have not changed, consider caching the result (`cachingToken` from `@nocobase/app-server/caching`) or skipping the run rather than recomputing it. +- Leave headroom in timeouts and leases. While a worker is alive, give it enough time to finish, for example by renewing its lease; a missed heartbeat is usually a more reliable sign that a machine is gone than a tight timeout, which can declare a slow but healthy worker dead and run its work twice. + ## Verify - The service resolves from the token and behaves correctly in isolation. diff --git a/packages/app/app-skills/skills/nocobase-app-development/references/testing.md b/packages/app/app-skills/skills/nocobase-app-development/references/testing.md index c8f1df55ac2..7a0f3341c2a 100644 --- a/packages/app/app-skills/skills/nocobase-app-development/references/testing.md +++ b/packages/app/app-skills/skills/nocobase-app-development/references/testing.md @@ -142,6 +142,8 @@ Then verify the affected behavior, selecting only the applicable steps below. Gr - Confirm the endpoint's responses for signed-out, unpermitted, and permitted callers. - Confirm `pnpm nocobase db apply` applies cleanly. +When a change is about performance, or adds a list, a statistic or a frequently called endpoint, measure it in a realistic environment: the database the application is actually deployed on, with a realistic data volume. A small data set hides slow queries and missing indexes, and SQLite's costs differ from those of a database server, so a timing on one says little about another. After release, consider each endpoint's call count multiplied by its duration rather than a single measurement: a cheap endpoint called constantly can cost more than a slow one called rarely. + ## Reporting Say what you ran, what passed, and what you did not run. If you could not verify something — no test database, a flow needing real credentials — say so rather than implying it was checked.