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, andmaxPixels. - Identify and fix common performance bottlenecks (loops,
getInfo(), unnecessaryreproject()). - 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.
- 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?
- Clip only for display. Clipping creates a mask for every pixel outside the boundary. For reducers and exports, pass the geometry directly instead.
- 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: 1e9as 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
tileScaleas 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.
Exportstatements set up the task, but don't execute it. We have to start it manually. This gets everyone at least once!
Quick self-check
- What does
tileScale: 4do in areduceRegion()call? - Why is
getInfo()inside a loop a problem? - What is the correct order for filtering a collection (spatial, temporal, metadata)?
- When should we use
Exportinstead of in-browser computation? - 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:
- EEFA Book - Chapter F6.2: Scaling Up in Earth Engine
- EEFA Book - Chapter F6.1: Collaborating in Earth Engine
- Google's Official Best Practices Guide
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.