Video Thumbnail for Lesson
9.1: Performance Best Practices

Performance Best Practices

By this point in the course we have written workflows, authored custom actions, and tuned the developer experience. This lesson focuses on the practices that keep those pipelines fast and responsive as your team and automation footprint grow.

1. Measure Performance first

Optimizing a workflow starts with understanding how long each part takes today. Export the built-in run timing data to an external system so you can visualize where the delays occur. Without a baseline it's impossible to know whether a change helped.

2. Spend less time waiting

  • Reduce runner queueing: Ensure you have sufficient capacity (hosted or managed) so jobs start quickly.
  • Fail fast: Order the most failure-prone tests first so developers get feedback before a long job fails near the end.

3. Do less work

  • Use on.push.paths/on.pull_request.paths filters, job-level if conditions, and step-level guards to avoid running when nothing relevant changed.
  • Lean on caching so you only rebuild what actually changed from run to run.

4. Use resources effectively

  • Split independent tasks into separate jobs so they can run in parallel.
  • Inside a job, break work across multiple cores where the tooling allows.
  • Avoid cross-architecture emulation (for example, using QEMU to build ARM64 images on AMD64 hosts) unless you truly need it because the performance penalty is significant. Many remote container build services get their speedups specifically by building natively per architecture and combining the images afterward.

5. Guard against regressions

Once you start improving performance, keep tracking the metrics. Feature work will inevitably add more tests and build steps; monitor timings so you can respond before slowdowns become a bottleneck.

Caching

Caching is one of the most effective tools for speeding up runs. Depending on your stack, consider layering several of these techniques:

  • Git checkout cache: Store a recent copy of the repository so actions/checkout only needs to fetch a small delta each run.
  • Language toolchains: If you install a custom version of Node, Go, Java, etc., cache the toolchain to skip repeated downloads.
  • Dependencies: Cache package manager directories (npm, pip, cargo, etc.) so you are not pulling dependencies over the public internet every time.
  • Build and test artifacts: Persist compiled assets or expensive test fixtures when their size makes sense.
  • Container layers: Keep base images hot, reuse unchanged layers, and use cache mounts to expose dependency caches inside Docker builds.