Here are some simple performance monitoring functions I’ve been using for all sorts of TS stuff. Really helps measure React functions, in addition to class-level methods that might be long-running. Includes conditional compilation to drop dead branches when building for prod.

// Define this in the build process in a Makefile maybe.
// It must be defined. It is the only way to do conditional
// compilation that I know of that is clean. The one issue
// is that you can't wrap it with other helper functions,
// or else we don't get dead-code elimination, because at
// compile time all evaluation involving the constant
// is replaced with booleans. Eg:
// `process.env.NODE_ENV !== "production"` becomes `true`,
// which drops the dead branch off of if-statements. Or at
// least it works this way with `esbuild`.
//
// The reason we want to drop it is because each timer fn
// invocation is not free, and in production if you're calling
// them alot, they add up. Useful for debugging, developing,
// but less useful in production.
declare const process: { env: { NODE_ENV: string } };

type DescriptorWrapper = (
  target: unknown,
  propKey: string,
  descriptor?: PropertyDescriptor,
) => PropertyDescriptor;

type NamedDescriptorWrapper = (name?: string) => DescriptorWrapper;

export type Timed<T> = (
  name: string,
  f: (...args: unknown[]) => T,
) => (...args: unknown[]) => T;

// Decorator/annotation for method to time how long it takes to run.
let Time: NamedDescriptorWrapper;

let interiorTimer: Timed<unknown>;

if (process.env.NODE_ENV === "production") {
  Time =
    (_?: string) =>
    (target: unknown, propKey: string, descriptor?: PropertyDescriptor) =>
      descriptor || Object.getOwnPropertyDescriptor(target, propKey);
  interiorTimer =
    <T>(name: string, f: (...args: unknown[]) => T) =>
    (...args) =>
      f(...args);
} else {
  Time = (name?: string) => {
    return (
      target: unknown,
      propKey: string,
      descriptor?: PropertyDescriptor,
    ) => {
      descriptor =
        descriptor || Object.getOwnPropertyDescriptor(target, propKey);
      const originalMethod = descriptor.value;
      descriptor.value = function (...args: unknown[]) {
        const prefix = (name ? name + "." : "") + propKey;
        const key = performance.now();
        const start = performance.mark(prefix + "-start-" + key);
        const result = originalMethod.apply(this, args);
        const end = performance.mark(prefix + "-end-" + key);
        performance.measure(prefix + "-" + key, start.name, end.name);
        return result;
      };
      return descriptor;
    };
  };
  interiorTimer =
    <T>(name: string, f: (...args: unknown[]) => T) =>
    (...args) => {
      const key = performance.now().toString();
      const start = performance.mark(name + "-start-" + key);
      const result = f(...args);
      const end = performance.mark(name + `-end-` + key);
      performance.measure(name + "-" + key, start.name, end.name);
      return result;
    };
}

// Wrap a function `f` as timed. Does NOT execute function though.
function timed<T>(
  name: string,
  f: (...args: unknown[]) => T,
): (...args: unknown[]) => T {
  return interiorTimer(name, f) as (...args: unknown[]) => T;
}

// Time one function that returns T.
function time<T>(name: string, f: (...args: unknown[]) => T): T {
  return (interiorTimer(name, f) as (...args: unknown[]) => T)();
}

export { TimePerformance, timed, time };

Use it like this:

class Thing {
  @Time("Thing")
  longRunningProcess() {
    // ...do big computation
  }

  another: () => void = timed("Thing.another", () => {
    // ...do another computation
  });

  plainFn() {
    time("interior", () => {
      // ...do big computation
    });
  }
}

I use them on React rendering functions sometimes, because it’s easier to understand performance outside the React dev tools extension, in the actual performance flame graph and timeline.