# Optimizing Sitecore Builds: CI/CD Performance Tips

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:

```yaml
- 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](https://learn.microsoft.com/en-us/azure/devops/pipelines/release/caching) in Azure DevOps can be implemented using the [Cache@2](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/cache-v2?view=azure-pipelines) 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:

```yaml
- 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](https://webpack.js.org/configuration/cache/).

```js
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.<br/>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_:

```yaml
- 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.

```Dockerfile
...
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.

```yaml
...
# 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:

```yaml
- 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.

```docker
...
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.

```docker
# 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.
