Optimizing Sitecore Builds: CI/CD Performance Tips

CI/CD Optimization in Sitecore

Building and deploying Sitecore solutions can be time-consuming if pipelines aren’t optimized. This article shares practical techniques we used to speed up CI/CD workflows for Sitecore XP (Kubernetes in our case) deployments, enabling faster and more efficient delivery.

In short

If you don’t want to read all the details, here are the main principles that can be applied to any CI/CD pipeline:

  • Do only what’s necessary — remove redundant tasks.
  • Use caching (node_modules, NuGet, etc.).
  • Reduce I/O load (e.g., file copies) — minimize the number of file transfers.

But… if you’re still interested in the details, read on.

Note: All implementation details here are for Azure DevOps pipelines, but the same principles apply to any CI/CD platform.


Run tasks conditionally

Our build pipeline is shared between regular builds (triggered on push) and deployment builds (used when deploying to an environment and generating deployment artifacts).

Note: We distinguish between those builds using the manual_release parameter. When it’s true (or checked), it triggers the next steps — image creation and Kubernetes deployment.

The simplest approach is to run certain steps conditionally:

  • Copy deployment-only files: e.g., Unicorn serialization files or configuration transforms.
  • Create and publish artifacts: ArchiveFiles@2, PublishBuildArtifacts@1
  • Publish debug symbols: PublishSymbols@2

Example:

- task: ArchiveFiles@2
  displayName: Zip Artifact Website
  ...
  condition: eq(variables.buildMode, 'Full') # execute this task only if buildMode is Full

Note: This optimization improves the average build performance by skipping unnecessary tasks during most builds (though it doesn’t affect worst-case build times).

Result: around 3–4 minutes faster per push-triggered build.


Caching

Caching is one of the most effective optimization techniques. With proper configuration, it can significantly improve performance.

Pipeline caching

Pipeline caching in Azure DevOps can be implemented using the Cache@2 task.

Useful cache targets include:

  • node_modules — if the cache restores successfully, there’s no need to reinstall packages.
  • The packages folder for NuGet — nuget restore still runs, but much faster.

Example:

- task: Cache@2
  displayName: node_modules Cache
  inputs:
    key: 'node_modules | "$(Agent.OS)" | frontend/package-lock.json'
    path: "frontend/node_modules"
    cacheHitVar: NODE_MODULES_CACHE_RESTORED

- task: Npm@1
  displayName: npm ci
  inputs:
    command: "ci"
    workingDir: "frontend"
  condition: ne(variables.NODE_MODULES_CACHE_RESTORED, 'true')

# just cache and restore Nuget packages fodler
- task: Cache@2
  inputs:
    key: 'nuget | "$(Agent.OS)" | Packages.props | nuget.config'
    path: packages
  displayName: Cache NuGet packages

Explanation: The cacheHitVar input sets NODE_MODULES_CACHE_RESTORED to true if the cache was restored.
Then the npm ci task only runs when the cache wasn’t restored — meaning no redundant package installation.

Frontend caching

Use filesystem caching to speed up continuous builds. In our case, we configured Webpack build cache.

const config = {
  ...,
  cache: {
    type: 'filesystem',
    name: '[PROJECT_NAME]',
    cacheDirectory: resolve(__dirname, 'node_modules', '.cache'),
    buildDependencies: {
      config: [__filename],
    },
  },
};

Note: The cache directory is stored inside node_modules, which means when the node_modules folder is restored from pipeline cache, Webpack can reuse its stored cache — resulting in faster local and CI/CD builds.

Result: Frontend build time (npm install + build) reduced from an average of 10 minutes to 3 minutes.


IO Operations

Our CI/CD setup uses a pipeline chain consisting of several stages:
Build → Solution Image → CM/CD Builds.

During this process, Unicorn serialization items must be included in the Docker images. This introduces a heavy I/O load, as a large number of .yaml files are copied between stages.

The flow looks like this:

# Step Action
1 Build Create an items artifact that contains all serialized Unicorn items.
2 Solution Image 1. Extract the artifact.
2. Copy all .yaml files into the Docker image.
3 CM & CD Builds Copy the .yaml files from the Solution image into the final CM and CD images.

Each stage involves file extraction and copying, leading to repeated I/O operations and slower overall build performance — especially when dealing with large numbers of serialized items.

Previously, it looked like this:

Pipeline:

- task: ExtractFiles@1
  displayName: Extract Artifact | Website
  inputs:
    archiveFilePatterns: $(Pipeline.Workspace)\build\$(artifact_prefix)_Website\*.zip
    destinationFolder: $(Build.SourcesDirectory)\backend\docker\build\solution.prebuild\artifacts\website

Solution Dockerfile — copying items from the extracted artifact into the image.

...
WORKDIR c:\prebuild
COPY artifacts\ .
RUN `
    Invoke-Expression 'robocopy C:\prebuild\website\App_Data\unicorn\src C:\out\items /s /ndl /nfl /njh /njs /nooffload /np *.yml'; `
...

CM Dockerfile — copying from the prebuild solution image into the CM image.

...
# Copy serialized items and Unicorn sync script
COPY --from=solution \artifacts\items\ \items\
...

This means there were a lot of copy operations.
The image build took up to 20 minutes due to the heavy I/O load from copying thousands of files.
The solution was to optimize it.

After optimization, it now looks like this:

- task: CopyFiles@2
  displayName: Copy Artifact | Website
  inputs:
    SourceFolder: $(Pipeline.Workspace)\build\$(artifact_prefix)_Website
    Contents: '*.zip'
    TargetFolder: $(Build.SourcesDirectory)\backend\docker\build\solution.prebuild\artifacts\website

Solution Dockerfile — just copy the zip archive into the Docker image.

...
WORKDIR c:\archives
COPY artifacts .
...

CM Dockerfile — keep the old copying for compatibility with local builds, but extract the zip if it’s a CI/CD build.

# Copy serialized items and Unicorn sync script
COPY --from=solution \artifacts\items\ \items\
# Extract archives
RUN if (Test-Path C:\archives\items.zip) { Invoke-Expression 'tar -xf C:\archives\items.zip -C C:\items' }

Result:

  • Solution image build time reduced from 20 minutes to 1 minute.
  • CM image build time reduced from 20 minutes to 10 minutes.

Conclusion

I hope the ideas described here help you optimize your CI/CD pipelines and reduce build times.