GEE Best Practices & Performance

Writing code that works is the first step. Writing code that works efficiently, is easy to read, and can be shared with collaborators? That's the real goal.

In this module, we'll cover the techniques that separate a beginner GEE script from a professional one. Let's level up.

Learning objectives

  • Optimize GEE scripts for speed and memory using tileScale, bestEffort, and maxPixels.
  • Identify and fix common performance bottlenecks (loops, getInfo(), unnecessary reproject()).
  • Organize code into reusable functions and modular scripts.
  • Document, share, and collaborate on scripts effectively using repositories and require().
  • Choose when to export results versus computing them in the browser.

Why it matters

GEE runs on shared cloud infrastructure with computation limits. A poorly written script can time out, exceed memory, or simply take minutes when it should take seconds.

Understanding performance also helps us work with larger areas, longer time series, and more complex analyses without hitting walls. Think of it like this: learning to drive is step one, but learning to navigate traffic efficiently is what gets you places.

Key vocabulary

Lazy evaluation
GEE does not execute code line by line. It builds a computation graph and only processes pixels when you request a result (print, map display, or export).
tileScale
A parameter that controls how GEE splits large computations into smaller tiles. Higher values use more tiles with less memory each.
bestEffort
When set to true, GEE automatically coarsens the scale if the computation exceeds memory at the requested resolution.
maxPixels
The maximum number of pixels a reducer will process. Prevents accidentally running a global-scale reduction at 10-meter resolution.

Quick Win: Slow vs. Fast (Before and After)

Let's start with a side-by-side comparison. The two scripts below perform the same analysis: computing mean NDVI across a large region. The first version is slow and may time out. The second applies three optimizations and runs reliably.

Spot the differences!

Before (slow, may time out)

// SLOW VERSION: common beginner mistakes
var roi = ee.FeatureCollection('TIGER/2018/States')
  .filter(ee.Filter.eq('NAME', 'Florida')).geometry();

var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-01-01', '2024-01-01');  // No spatial filter!

var ndvi = collection.map(function(img) {
  var scaled = img.multiply(0.0000275).add(-0.2);  // Scaling ALL bands
  return scaled.normalizedDifference(['SR_B5', 'SR_B4']);
});

var composite = ndvi.median().clip(roi);  // Clip before reduce

var stats = composite.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: roi,
  scale: 30
  // Missing maxPixels!
});
print('Mean NDVI:', stats);

After (fast, reliable)

// FAST VERSION: three key optimizations
var roi = ee.FeatureCollection('TIGER/2018/States')
  .filter(ee.Filter.eq('NAME', 'Florida')).geometry();

var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterBounds(roi)                           // 1. Spatial filter first
  .filterDate('2023-01-01', '2024-01-01')
  .select(['SR_B5', 'SR_B4']);                 // 2. Select only needed bands

var ndvi = collection.map(function(img) {
  var scaled = img.multiply(0.0000275).add(-0.2);
  return scaled.normalizedDifference(['SR_B5', 'SR_B4']);
});

var composite = ndvi.median();                 // 3. Don't clip before reduce

var stats = composite.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: roi,
  scale: 30,
  maxPixels: 1e9,
  tileScale: 4                                // Splits into smaller tiles
});
print('Mean NDVI:', stats);

What you should see

Both scripts compute the same mean NDVI for Florida, but the optimized version completes in seconds while the slow version may time out or trigger a memory error. The console prints a dictionary with the mean NDVI value (approximately 0.4 to 0.5).

The Limits We're Working With

GEE enforces three main limits on interactive (in-browser) computations. It's important to know what they are so we can work within them:

Limit Threshold What Happens
Computation time ~5 minutes per request "Computation timed out" error
User memory Variable (depends on load) "User memory limit exceeded" error
maxPixels Default: 10,000,000 "Too many pixels" error if not set higher

These limits exist because GEE is a shared resource. Our script runs alongside thousands of other users' scripts. Writing efficient code isn't just good practice; it's a requirement for working at scale.

Three Bottlenecks That Slow Everyone Down

Most slow scripts share a few recurring problems. Learning to spot them will save us hours of debugging. Let's walk through the big three.

1. Using getInfo() in loops

Each getInfo() call forces the browser to wait for the server to finish a computation. Inside a loop, this creates a serial chain of blocking calls. Think of it like standing in line at a coffee shop and placing one order at a time, waiting for each cup before ordering the next.

// BAD: getInfo() in a loop (extremely slow)
var years = [2018, 2019, 2020, 2021, 2022, 2023];
for (var i = 0; i < years.length; i++) {
  var count = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .filterDate(years[i] + '-01-01', years[i] + '-12-31')
    .size().getInfo();          // Blocks on every iteration!
  print(years[i] + ': ' + count);
}

// GOOD: Server-side mapping (parallel, fast)
var yearList = ee.List([2018, 2019, 2020, 2021, 2022, 2023]);
var counts = yearList.map(function(year) {
  year = ee.Number(year);
  var count = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .filterDate(
      ee.Date.fromYMD(year, 1, 1),
      ee.Date.fromYMD(year, 12, 31))
    .size();
  return ee.List([year, count]);
});
print('Image counts by year:', counts);

2. Unnecessary reproject()

Calling .reproject() forces GEE to materialize the image at a specific projection before continuing. This prevents the optimizer from choosing efficient tile sizes and often causes memory errors.

Only use reproject() when you have a specific, well-understood reason (for example, to fix convolution kernel alignment). Otherwise, let GEE handle projections automatically.

3. Processing large geometries without spatial filtering

Loading an entire global collection when we only need one state wastes enormous resources. Always apply .filterBounds() before any other operation. It's the single biggest optimization you can make.

Our Optimization Toolkit

tileScale

The tileScale parameter tells GEE to split the computation into smaller tiles. A value of 4 means each tile is 1/4 the default size, using less memory per tile. Values range from 1 (default) to 16. It's like cutting a pizza into more slices so each piece is easier to handle.

var stats = image.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: largeRegion,
  scale: 30,
  maxPixels: 1e9,
  tileScale: 4    // Use smaller tiles to avoid memory errors
});

bestEffort

When bestEffort is true, GEE automatically coarsens the resolution if the computation would exceed memory at the requested scale. This is great for quick exploratory analyses where exact resolution is less important than getting a result.

var stats = image.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: largeRegion,
  scale: 30,
  bestEffort: true   // Auto-adjust scale if needed
});

Pre-filtering and lazy evaluation

GEE uses lazy evaluation: it doesn't compute anything until we request output. This means the order of our operations matters. Filters applied early reduce the amount of data the system must process later.

Here's the golden rule for filter order:

// Optimal filter order: bounds → date → metadata → select
var efficient = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterBounds(roi)                          // Spatial first (biggest reduction)
  .filterDate('2023-06-01', '2023-09-01')     // Then temporal
  .filter(ee.Filter.lt('CLOUD_COVER', 10))    // Then metadata
  .select(['SR_B4', 'SR_B5']);                // Then bands

Three Habits That Save Memory

These three simple habits dramatically reduce memory consumption. Once they become second nature, you'll rarely hit memory errors again.

  1. Select bands early. If we only need NIR and Red for NDVI, let's drop everything else at the start. Why carry baggage we don't need?
  2. Clip only for display. Clipping creates a mask for every pixel outside the boundary. For reducers and exports, pass the geometry directly instead.
  3. Use .filterBounds() instead of global collections. A collection filtered to our region loads orders of magnitude less data.
// Memory-efficient pattern
var ndvi = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterBounds(roi)
  .filterDate('2023-01-01', '2024-01-01')
  .select(['SR_B4', 'SR_B5'])                 // Only 2 bands, not 19
  .map(function(img) {
    var scaled = img.multiply(0.0000275).add(-0.2);
    return scaled.normalizedDifference(['SR_B5', 'SR_B4']);
  })
  .median();

// Pass geometry to reducer (no .clip() needed)
var meanNDVI = ndvi.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: roi,
  scale: 30,
  maxPixels: 1e9
});
print('Mean NDVI:', meanNDVI);

Keeping Your Code Clean

Well-organized code is easier to debug, modify, and share. As our scripts grow, these conventions keep everything manageable. Let's look at two key strategies.

Use functions for repeated operations

// Define reusable functions at the top of your script
function applyScaleFactors(image) {
  var optical = image.select('SR_B.*').multiply(0.0000275).add(-0.2);
  var thermal = image.select('ST_B.*').multiply(0.00341802).add(149.0);
  return image.addBands(optical, null, true)
              .addBands(thermal, null, true);
}

function addNDVI(image) {
  var ndvi = image.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI');
  return image.addBands(ndvi);
}

// Use them in your workflow
var processed = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterBounds(roi)
  .filterDate('2023-01-01', '2024-01-01')
  .map(applyScaleFactors)
  .map(addNDVI);

A script structure template

A consistent layout makes every script predictable. Here's a template we can follow:

// ============================================
// Title: NDVI Time Series for Alachua County
// Author: Your Name
// Date: 2023-10-15
// Description: Computes monthly NDVI composites
//   for Alachua County, FL using Landsat 8.
// ============================================

// --- 1. PARAMETERS ---
var startDate = '2023-01-01';
var endDate = '2024-01-01';
var cloudThreshold = 20;

// --- 2. STUDY AREA ---
var roi = ee.FeatureCollection('TIGER/2018/Counties')
  .filter(ee.Filter.eq('NAME', 'Alachua'));

// --- 3. FUNCTIONS ---
// (define all helper functions here)

// --- 4. DATA LOADING & PROCESSING ---
// (load, filter, map functions)

// --- 5. VISUALIZATION ---
// (Map.addLayer, charts)

// --- 6. EXPORTS ---
// (Export.image, Export.table)

Sharing Your Work With Others

Science is collaborative, and GEE gives us several ways to share our work with collaborators, reviewers, or the public. Let's walk through the options.

Get Link

Click the "Get Link" button in the Code Editor to generate a shareable URL. The recipient gets a copy of your script (not a live link). Any changes they make won't affect your original. It's the quickest way to share.

Script repositories

Repositories are like folders that can be shared with specific users or made public. Create a repository under your username and invite collaborators with read or write access. This is the best approach for team projects.

Modules with require()

Here's where it gets interesting. For code we want to reuse across multiple scripts, we can create a module. Think of it like a file path: a module is simply a script that exports functions or variables, and any other script can import them.

// FILE: users/yourname/modules:utils
// This is a reusable module

exports.applyScaleFactors = function(image) {
  var optical = image.select('SR_B.*').multiply(0.0000275).add(-0.2);
  var thermal = image.select('ST_B.*').multiply(0.00341802).add(149.0);
  return image.addBands(optical, null, true)
              .addBands(thermal, null, true);
};

exports.addNDVI = function(image) {
  var ndvi = image.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI');
  return image.addBands(ndvi);
};
// FILE: Your analysis script
// Import the module
var utils = require('users/yourname/modules:utils');

var processed = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterBounds(roi)
  .filterDate('2023-01-01', '2024-01-01')
  .map(utils.applyScaleFactors)
  .map(utils.addNDVI);

Browser or Export? Choosing the Right Approach

Not every computation should run in the browser. Here's a handy guide for choosing between interactive computation and batch export.

Scenario Approach Why
Quick preview of a single scene In-browser (Map, print) Fast, immediate feedback
Computing statistics for a small region In-browser (reduceRegion) Result is small, completes quickly
Processing a full state or country Export (Export.image.toDrive) Too large for interactive limits
Time series of 10+ years Export (Export.table.toDrive) Many images, long computation
Sharing results with non-GEE users Export to Drive or Cloud Storage GeoTIFF/CSV can be opened in any GIS
// Export a large composite to Google Drive
Export.image.toDrive({
  image: ndviComposite,
  description: 'NDVI_Florida_2023',
  folder: 'GEE_Exports',
  region: roi,
  scale: 30,
  maxPixels: 1e9,
  crs: 'EPSG:4326'
});

After running the script, switch to the Tasks tab and click "Run" next to your export. Exports run in the background and don't count against interactive computation limits. Pretty convenient!

Pro tips

  • Profile your script. Open the browser developer console (F12) and check the Network tab to see how many requests GEE makes. Fewer requests = faster script.
  • Use .aside(print) for debugging. It prints the object and returns it, so we can insert it into a chain without breaking it: collection.aside(print).map(func). It's like peeking inside a pipeline.
  • Avoid reproject() unless absolutely necessary. It forces materialization and often triggers memory errors. Let GEE handle projections automatically.
  • Set maxPixels: 1e9 as a habit. The default of 10 million pixels is too low for most real analyses.
  • Test on small areas first. Develop your script on a single county, then scale to the full study area once the logic works. Don't try to go global on the first run!

Try it: Optimize a slow script

The script below has several performance problems. Can you spot and fix at least three issues? Give it a try before looking at the comments.

// This script is intentionally slow. Fix it!
var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-01-01', '2024-01-01');
  // Problem 1: no spatial filter

var ndvi = collection.map(function(img) {
  return img.normalizedDifference(['SR_B5', 'SR_B4']);
  // Problem 2: no scale factors applied
  // Problem 3: processing all bands, not just B5 and B4
});

var roi = ee.FeatureCollection('TIGER/2018/States')
  .filter(ee.Filter.eq('NAME', 'Texas')).geometry();
var composite = ndvi.median().clip(roi);
  // Problem 4: clipping before reduce

var stats = composite.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: roi,
  scale: 30
  // Problem 5: missing maxPixels and tileScale
});
print(stats);

Common mistakes

  • Using tileScale as a magic fix. It helps with memory, but it doesn't fix fundamentally inefficient code. Always optimize your filters and band selections first.
  • Exporting everything. Not every result needs to be a GeoTIFF. If we just need a number or a chart, keep it in the browser.
  • Putting functions inside .map() calls. Define functions outside and pass them by reference. This improves readability and allows reuse.
  • Forgetting to click "Run" in the Tasks tab. Export statements set up the task, but don't execute it. We have to start it manually. This gets everyone at least once!

Quick self-check

  1. What does tileScale: 4 do in a reduceRegion() call?
  2. Why is getInfo() inside a loop a problem?
  3. What is the correct order for filtering a collection (spatial, temporal, metadata)?
  4. When should we use Export instead of in-browser computation?
  5. How do we import a function from a GEE module?

Going deeper

We've covered the most common optimization and organization techniques. When you're ready for advanced scaling strategies and collaborative workflows, check out:

Next Steps

  • Batch Exports - set up large-scale exports to Drive and Cloud Storage.
  • Debugging Guide - diagnose and fix common GEE errors.
  • Reducers - compute summary statistics efficiently at scale.