Last month, one of my pull requests was merged into the popular community project, React Native Camera, in response to a tweet from one of the maintainers. React Native Camera’s builds were blocked because they were trying to use a resource class not available on the free/OSS plans. On top of unblocking them, I made some additional optimizations.

The tactics I used in this PR can be generalized and used to optimize many other projects. In this post, I’ll break down some of the changes I made to the React Native Camera project, explain how they improved performance, and suggest ways that you can use similar techniques to improve your own projects.

Caching step placement

Generally, people have a habit of placing their save_cache statements at the end of their build. React Native Camera was doing the same, like so:

jobs:
  build-app:
    # ...environment setup here
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-npm-{{ .Branch }}-{{ checksum "yarn.lock" }}
            - v1-npm
      - run:
          name: Install Dependencies
          command: yarn install --ignore-engines
      # ...other steps, etc.
      - save_cache:
          key: v1-npm
          paths:
            - node_modules/
      - save_cache:
          key: v1-npm-{{ .Branch }}-{{ checksum "yarn.lock" }}
          paths:
            - node_modules/
      # ...rest of config

This is not optimal because saving the cache will not happen if any of the other steps in the job fail before the save_cache step has a chance to execute. It’s best to place the save_cache step immediately after a dependency step - this guarantees the cache will be saved as long as the dependency step works:

jobs:
  build-app:
    # ...environment setup
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-npm-{{ .Branch }}-{{ checksum "yarn.lock" }}
            - v1-npm
      - run:
          name: Install Dependencies
          command: yarn install --ignore-engines
      #### MOVED
      - save_cache:
          key: v1-npm
          paths:
            - node_modules/
      - save_cache:
          key: v1-npm-{{ .Branch }}-{{ checksum "yarn.lock" }}
          paths:
            - node_modules/
      #### /MOVED
      # ...rest of config

You can find out more information about caching and see examples in our docs.

Adding caching for Gradle

Another change I made was adding caching for Gradle dependencies. Their Run Checks and Build Sample App steps in CircleCI were Gradle Wrapper executions, and they were taking up the majority of the build times. In the config below, I store the Gradle cache as well as the wrapper.

jobs:
  build-app:
    # ...environment setup
    steps:
      # ...previous build steps
      #### ADDED
      - restore_cache:
          keys: 
            - v1-gradle-{{ checksum "android/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "examples/basic/android/gradle/wrapper/gradle-wrapper.properties" }}
            - v1-gradle-wrapper
      - restore_cache:
          keys:
            - v1-gradle-cache-{{ checksum "android/build.gradle" }}-{{ checksum "examples/basic/android/build.gradle" }}
            - v1-gradle-cache
      #### /ADDED
      - run:
          name: Run Checks
          command: |
            cd android
            chmod +x ./gradlew && ./gradlew check
      # ...other step(s)
      - run:
          name: Run Yarn to Generate react.gradle
          command: cd examples/basic/android && yarn
      - run:
          name: Build Sample App
          command: |
            cd examples/basic/android && chmod +x ./gradlew && ./gradlew build
      #### ADDED
      - save_cache:
          key: v1-gradle-wrapper-{{ checksum "examples/basic/android/gradle/wrapper/gradle-wrapper.properties" }}
          paths:
            - ~/.gradle/wrapper
      - save_cache:
          key: v1-gradle-cache-{{ checksum "examples/basic/android/build.gradle" }}
          paths:
            - ~/.gradle/caches
      #### /ADDED
      # ...rest of config

Making artifacts from reports

Did you know that our artifacts act like a static file server? This is useful for storing things like HTML reports for your test suites and accessing them like web pages. I added an artifact step to store the generated output of the linting commands. This allows you to browse artifacts on the artifacts tab, and access those files like they are webpages.

jobs:
  build-app:
    # ...environment setup
    steps:
      # ...previous build steps
      - run:
          name: Run Checks
          command: |
            cd android
            chmod +x ./gradlew && ./gradlew check
      #### ADDED
      - store_artifacts:
          path: android/build/reports
      #### /ADDED
      # ...other step(s)
      - run:
          name: Build Sample App
          command: |
            cd examples/basic/android && chmod +x ./gradlew && ./gradlew build
      #### ADDED
      - store_artifacts:
          path: examples/basic/android/app/build/reports
          destination: app
      #### /ADDED
      # ...rest of config

You can find more information about creating artifacts in our docs.

Conclusion

The small changes made in this pull request are just a few examples of features you could make use of to optimize your CircleCI builds. Documentation for our platform is extensive, with various examples and tutorials. The CircleCI blog is also a great resource. And if you get stuck, try tweeting to us @CircleCI!

For more optimization features and techniques read our Optimizing open source project builds on CircleCI post.

Thanks for reading, and happy building!