> For the complete documentation index, see [llms.txt](https://triton.pharos.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://triton.pharos.ai/beyond-apex/profiling/custom-marks.md).

# Custom Performance Marks

Drop browser-style performance marks anywhere in your LWC code with startPerformanceMark / endPerformanceMark — fully per-instance and Locker Service-safe.

The high-level helpers (`timeBackendCall`, `timeUserInteraction`, `trackComponentLifecycle`, `trackComponentRender`) cover the common cases. When you need to time something that doesn't fit those moulds — a chunk of client-side data shaping, an animation, a debounce window, a delay between async steps — reach for raw performance marks.

The API mirrors the browser's `performance.mark()` pattern:

{% code lineNumbers="true" %}

```javascript
this.triton.startPerformanceMark('product-search');
// ... do work ...
this.triton.endPerformanceMark('product-search');
```

{% endcode %}

`startPerformanceMark` stores a starting timestamp keyed by mark name and component instance, pushes a span for the mark, and publishes an opening `TYPE.PERFORMANCE` log entry. `endPerformanceMark` computes the duration, publishes the matching completion entry on the same span, and clears the mark.

## When to Reach for Marks

Marks are the right tool when:

* You're measuring purely client-side work that has no backend round trip — e.g. parsing a CSV, computing a derived data structure, an animation timeline.
* You want to wrap *both sides* of an async boundary that the higher-level helpers don't model — e.g. starting a timer when a debounce fires, stopping it when the next user event arrives.
* You want to time a stretch of work that isn't a backend call or a discrete user interaction, without the error handling and result wrapping `timeBackendCall` layers on top.

If your work *is* an Apex call or a user-triggered flow, the higher-level helpers are almost always a better fit — they give you standard error handling and result wrapping for free.

## Example: Wrapping a Search Flow

{% code lineNumbers="true" %}

```javascript
async handleSearch() {
    const searchTerm = this.searchInput.value;

    this.triton.startPerformanceMark('product-search');
    try {
        const results = await this.triton.timeBackendCall(
            'searchProducts',
            () => searchProducts({ searchTerm })
        ).execute();

        this.products = results;
    } finally {
        // Always pair the start with an end — even on failure.
        this.triton.endPerformanceMark('product-search');
    }
}
```

{% endcode %}

This pattern is handy when you want a single "user-perceived search latency" record (the mark) plus the more granular backend call timing inside it. The mark captures the whole stretch — including the time between the backend response and the UI being ready — while the backend call captures just the round trip.

{% hint style="info" %}
You can have multiple marks active at once. Names are independent: `startPerformanceMark('search')` and `startPerformanceMark('hydrate')` coexist happily and end independently.
{% endhint %}

## Per-Instance Isolation

Marks are stored per component instance, just like lifecycle and render counts. Two instances of the same component can each have an active `product-search` mark and neither will interfere with the other.

{% code lineNumbers="true" %}

```javascript
export default class ProductSearch extends LightningElement {
    triton;

    connectedCallback() {
        this.triton = new Triton().bindToComponent('ProductSearch');
        this.triton.trackComponentLifecycle('connected');
    }

    async search() {
        // Each instance's mark is isolated.
        this.triton.startPerformanceMark('search-operation');

        const results = await this.triton.timeBackendCall(
            'searchProducts',
            () => searchProducts({ term: this.searchTerm })
        ).execute();

        this.triton.endPerformanceMark('search-operation');
        this.products = results;
    }
}
```

{% endcode %}

## What Gets Logged

A mark emits a **pair** of log entries — one when it opens, one when it closes:

| Field          | Opening entry                                       | Completion entry                            |
| -------------- | --------------------------------------------------- | ------------------------------------------- |
| `type`         | `TYPE.PERFORMANCE`                                  | `TYPE.PERFORMANCE`                          |
| `summary`      | `Performance started: <componentName> - <markName>` | `Performance: <componentName> - <markName>` |
| `details`      | *(unset)*                                           | `Duration: <ms>ms`                          |
| `duration`     | *(unset)*                                           | the measured milliseconds                   |
| `action`       | the mark name                                       | the mark name                               |
| `spanId`       | the mark's span                                     | the **same** span                           |
| `parentSpanId` | the enclosing span                                  | the **same** enclosing span                 |

The two records share a span id, and the backend pairs them into a single span. The reason `startPerformanceMark` publishes immediately is that the span has to exist while the mark is open: anything logged in between is parented to it, and a mark left open by an unmounted component, a navigation, or an exception would otherwise leave those logs pointing at a span that never arrives.

If you call `endPerformanceMark` for a name that was never started, Triton emits a console warning and skips the log.

{% hint style="info" %}
**Same concept in Apex.** `Triton.startMark()` / `Triton.endMark()` bring this pattern to Apex, with the same paired records and the same summary prefixes — see [Performance Marks in Apex](/common-apex-usage-patters/performance-marks.md). The names differ by tier: LWC uses `startPerformanceMark` / `endPerformanceMark`, Apex uses `startMark` / `endMark`.
{% endhint %}

## Locker Service Compatibility

Mark timing uses the same `getCurrentTime()` helper described in [Component Lifecycle](/beyond-apex/profiling/component-lifecycle.md#locker-service-safe-timing): `performance.now()` when available, `Date.now()` otherwise. You get sub-millisecond precision where allowed, and you never need a fallback path in your code.

## Best Practices

* **Always pair `startPerformanceMark` with `endPerformanceMark`.** Wrap the end in `finally` so an exception in the middle doesn't leave a dangling mark.
* **Use descriptive, hyphenated names.** They show up directly in dashboards (`action` column).
* **Bind first.** Custom marks require `bindToComponent()` — without it you'll just get a console warning and no log.
* **Don't double up.** If you're already wrapping with `timeBackendCall` or `timeUserInteraction`, you usually don't need a custom mark on top — the helpers already record duration.
