Cesium is one of the largest JavaScript codebases in the world. Since its start, we have maintained a high standard for code quality, which has made the codebase easier to work with for both new and experienced contributors. We hope you find the codebase to be clean and consistent.
In addition to describing typical coding conventions, this guide also covers best practices for design, maintainability, and performance. It is the cumulative advice of many developers after years of production development, research, and experimentation.
🎨 The color palette icon indicates a design tip.
🏠 The house icon indicates a maintainability tip. The whole guide is, of course, about writing maintainable code.
🚤 The speedboat indicates a performance tip.
To some extent, this guide can be summarized as make new code similar to existing code.
- Naming
- Formatting
- Units
- Basic Code Construction
- Functions
- Classes
- Design
- Third-Party Libraries
- GLSL
- Resources
- Directory names are
PascalCase
, e.g.,Source/Scene
. - Constructor functions are
PascalCase
, e.g.,Cartesian3
. - Functions are
camelCase
, e.g.,defaultValue()
,Cartesian3.equalsEpsilon()
. - Files end in
.js
and have the same name as the JavaScript identifier, e.g.,Cartesian3.js
anddefaultValue.js
. - Variables, including class properties, are
camelCase
, e.g.,
this.minimumPixelSize = 1.0; // Class property
var bufferViews = gltf.bufferViews; // Local variable
- Private (by convention) members start with an underscore, e.g.,
this._canvas = canvas;
- Constants are in uppercase with underscores, e.g.,
Cartesian3.UNIT_X = freezeObject(new Cartesian3(1.0, 0.0, 0.0));
- Avoid abbreviations in public identifiers unless the full name is prohibitively cumbersome and has a widely accepted abbreviation, e.g.,
Cartesian3.maximumComponent() // Not Cartesian3.maxComponent()
Ellipsoid.WGS84 // Not Ellipsoid.WORLD_GEODETIC_SYSTEM_1984
- Prefer short and descriptive names for local variables, e.g., if a function has only one length variable,
var primitivesLength = primitives.length;
is better written as
var length = primitives.length;
- When accessing an outer-scope's
this
in a closure, name the variablethat
, e.g.,
var that = this;
this._showTouch = createCommand(function() {
that._touch = true;
});
A few more naming conventions are introduced below along with their design pattern, e.g., options
parameters, result
parameters and scratch variables, and from
constructors.
In general, format new code in the same way as the existing code.
- Use four spaces for indentation. Do not use tab characters.
- Do not include trailing whitespace.
- Put
{
on the same line as the previous statement:
function defaultValue(a, b) {
// ...
}
if (!defined(result)) {
// ...
}
- Use curly braces even for single line
if
,for
, andwhile
blocks, e.g.,
if (!defined(result))
result = new Cartesian3();
is better written as
if (!defined(result)) {
result = new Cartesian3();
}
- Use parenthesis judiciously, e.g.,
var foo = x > 0.0 && y !== 0.0;
is better written as
var foo = (x > 0.0) && (y !== 0.0);
- Use vertical whitespace to separate functions and to group related statements within a function, e.g.,
function Model(options) {
// ...
this._allowPicking = defaultValue(options.allowPicking, true);
this._ready = false;
this._readyPromise = when.defer();
// ...
};
- In JavaScript code, use single quotes,
'
, instead of double quotes,"
. In HTML, use double quotes.
- Cesium uses SI units:
- meters for distances,
- radians for angles, and
- seconds for time durations.
- If a function has a parameter with a non-standard unit, such as degrees, put the unit in the function name, e.g.,
Cartesian3.fromDegrees = function(longitude, latitude, height, ellipsoid, result) { /* ... */ }
- Cesium uses JavaScript's strict mode so each module (file) contains
'use strict';
- 🚤 To avoid type coercion (implicit type conversion), test for equality with
===
and!==
, e.g.,
var i = 1;
if (i === 1) {
// ...
}
if (i !== 1) {
// ...
}
- To aid the human reader, append
.0
to whole numbers intended to be floating-point values, e.g., unlessf
is an integer,
var f = 1;
is better written as
var f = 1.0;
- Declare variables where they are first used. For example,
var i;
var m;
var models = [ /* ... */ ];
var length = models.length;
for (i = 0; i < length; ++i) {
m = models[i];
// Use m
}
is better written as
var models = [ /* ... */ ];
var length = models.length;
for (var i = 0; i < length; ++i) {
var m = models[i];
// Use m
}
- Variables have function-level, not block-level scope. Do not rely on variable hoisting, i.e., using a variable before it is declared, e.g.,
console.log(i); // i is undefined here. Never use a variable before it is declared.
var i = 0.0;
- 🚤 Avoid redundant nested property access. This
scene._state.isSkyAtmosphereVisible = true
scene._state.isSunVisible = true;
scene._state.isMoonVisible = false;
is better written as
var state = scene._state;
state.isSkyAtmosphereVisible = true
state.isSunVisible = true;
state.isMoonVisible = false;
- Do not create a local variable that is used only once unless it significantly improves readability, e.g.,
function radiiEquals(left, right) {
var leftRadius = left.radius;
var rightRadius = right.radius;
return (leftRadius === rightRadius);
}
is better written as
function radiiEquals(left, right) {
return (left.radius === right.radius);
}
- Use
undefined
instead ofnull
. - Test if a variable is defined using Cesium's
defined
function, e.g.,
var v = undefined;
if (defined(v)) {
// False
}
var u = {};
if (defined(u)) {
// True
}
- Use Cesium's
definedNotNull
function to test the return ofString.match()
and JSON properties. - Use Cesium's
freezeObject
function to create enums, e.g.,
/*global define*/
define([
'../Core/freezeObject'
], function(
freezeObject) {
"use strict";
var ModelAnimationState = {
STOPPED : 0,
ANIMATING : 1
};
return freezeObject(ModelAnimationState);
});
- Use descriptive comments for non-obvious code, e.g.,
byteOffset += sizeOfUint32; // Add 4 to byteOffset
is better written as
byteOffset += sizeOfUint32; // Skip length field
TODO
comments need to be removed or addressed before the code is merged into master. Used sparingly,PERFORMANCE_IDEA
, can be handy later when profiling.- Remove commented out code before merging into master.
- 🎨 Functions should be cohesive; they should only do one task.
- Statements in a function should be at a similar level of abstraction. If a code block is much lower level than the rest of the statements, it is a good candidate to move to a helper function, e.g.,
Cesium3DTileset.prototype.update = function(frameState) {
var tiles = this._processingQueue;
var length = tiles.length;
for (var i = length - 1; i >= 0; --i) {
tiles[i].process(this, frameState);
}
selectTiles(this, frameState);
updateTiles(this, frameState);
};
is better written as
Cesium3DTileset.prototype.update = function(frameState) {
processTiles(this, frameState);
selectTiles(this, frameState);
updateTiles(this, frameState);
};
function processTiles(tiles3D, frameState) {
var tiles = tiles3D._processingQueue;
var length = tiles.length;
for (var i = length - 1; i >= 0; --i) {
tiles[i].process(tiles3D, frameState);
}
}
- Do not use an unnecessary
else
block at the end of a function, e.g.,
function getTransform(node) {
if (defined(node.matrix)) {
return Matrix4.fromArray(node.matrix);
} else {
return Matrix4.fromTranslationQuaternionRotationScale(node.translation, node.rotation, node.scale);
}
}
is better written as
function getTransform(node) {
if (defined(node.matrix)) {
return Matrix4.fromArray(node.matrix);
}
return Matrix4.fromTranslationQuaternionRotationScale(node.translation, node.rotation, node.scale);
}
- 🚤 Smaller functions are more likely to be optimized by JavaScript engines. Consider this for code that is likely to be a hot spot.
🎨 Many Cesium functions take an options
parameter to support optional parameters, self-documenting code, and forward compatibility. For example, consider:
var sphere = new SphereGeometry(10.0, 32, 16, VertexFormat.POSITION_ONLY);
It is not clear what the numeric values represent, and the caller needs to know the order of parameters. If this took an options
parameter, it would look like this:
var sphere = new SphereGeometry({
radius : 10.0,
stackPartitions : 32,
slicePartitions : 16,
vertexFormat : VertexFormat.POSITION_ONLY
});
- 🚤 Using
{ /* ... */ }
creates an object literal, which is a memory allocation. Avoid designing functions that use anoptions
parameter if the function is likely to be a hot spot; otherwise, callers will have to use a scratch variable (see below) for performance. Constructor functions for non-math classes are good candidates foroptions
parameters since Cesium avoids constructing objects in hot spots. For example,
var p = new Cartesian3({
x : 1.0,
y : 2.0,
z : 3.0
});
is a bad design for the Cartesian3
constructor function since its performance is not as good as that of
var p = new Cartesian3(1.0, 2.0, 3.0);
If a sensible default exists for a function parameter or class property, don't require the user to provide it. Use Cesium's defaultValue
to assign a default value. For example, height
defaults to zero in Cartesian3.fromRadians
:
Cartesian3.fromRadians = function(longitude, latitude, height) {
height = defaultValue(height, 0.0);
// ...
};
- 🚤 Don't use
defaultValue
if it could cause an unnecessary function call or memory allocation, e.g.,
this._mapProjection = defaultValue(options.mapProjection, new GeographicProjection());
is better written as
this._mapProjection = defined(options.mapProjection) ? options.mapProjection : new GeographicProjection();
- If an
options
parameter is optional, usedefaultValue.EMPTY_OBJECT
, e.g.,
function DebugModelMatrixPrimitive(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
this.length = defaultValue(options.length, 10000000.0);
this.width = defaultValue(options.width, 2.0);
// ...
}
Some common sensible defaults are
height
:0.0
ellipsoid
:Ellipsoid.WGS84
show
:true
- Throw Cesium's
DeveloperError
when the user has a coding error. The most common errors are missing parameters and out-of-range parameters. For example:
Cartesian3.maximumComponent = function(cartesian) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError('cartesian is required.');
}
//>>includeEnd('debug');
return Math.max(cartesian.x, cartesian.y, cartesian.z);
};
- To check for
DeveloperError
, surround code inincludeStart
/includeEnd
comments, as shown above, so developer error checks can be optimized out of release builds. Do not include required side effects insideincludeStart
/includeEnd
, e.g.,
Cartesian3.maximumComponent = function(cartesian) {
//>>includeStart('debug', pragmas.debug);
var c = cartesian;
if (!defined(c)) {
throw new DeveloperError('cartesian is required.');
}
//>>includeEnd('debug');
// Works in debug. Fails in release since c is optimized out!
return Math.max(c.x, c.y, c.z);
};
- Throw Cesium's
RuntimeError
for an error that will not be known until runtime. Unlike developer errors, runtime error checks are not optimized out of release builds.
if (typeof WebGLRenderingContext === 'undefined') {
throw new RuntimeError('The browser does not support WebGL.');
}
- 🎨 Exceptions are exceptional. Avoid throwing exceptions, e.g., if a polyline is only provided one position, instead of two or more, instead of throwing an exception just don't render it.
🚤 In JavaScript, user-defined classes such as Cartesian3
are reference types and are therefore allocated on the heap. Frequently allocating these types causes a significant performance problem because it creates GC pressure, which causes the Garbage Collector to run longer and more frequently.
Cesium uses required result
parameters to avoid implicit memory allocation. For example,
var sum = Cartesian3.add(v0, v1);
would have to implicitly allocate a new Cartesian3
object for the returned sum. Instead, Cartesian3.add
requires a result
parameter:
var result = new Cartesian3();
var sum = Cartesian3.add(v0, v1, result); // Result and sum reference the same object
This makes allocations explicit to the caller, which allows the caller to, for example, reuse the result object in a file-scoped scratch variable:
var scratchDistance = new Cartesian3();
Cartesian3.distance = function(left, right) {
Cartesian3.subtract(left, right, scratchDistance);
return Cartesian3.magnitude(scratchDistance);
};
The code is not as clean, but the performance improvement is often dramatic.
As described below, from
constructors also use optional result
parameters.
- 🎨 Classes should be cohesive. A class should represent one abstraction.
- 🎨 Classes should be loosely coupled. Two classes should not be entangled and rely on each other's implementation details; they should communicate through well-defined interfaces.
- Create a class by creating a constructor function:
function Cartesian3(x, y, z) {
this.x = defaultValue(x, 0.0);
this.y = defaultValue(y, 0.0);
this.z = defaultValue(z, 0.0);
};
- Create an instance of a class (an object) by calling the constructor function with
new
:
var p = new Cartesian3(1.0, 2.0, 3.0);
- 🚤 Assign to all the property members of a class in the constructor function. This allows JavaScript engines to use a hidden class and avoid entering dictionary mode. Assign
undefined
if no initial value makes sense. Do not add properties to an object, e.g.,
var p = new Cartesian3(1.0, 2.0, 3.0);
p.w = 4.0; // Adds the w property to p, slows down property access since the object enters dictionary mode
- 🚤 For the same reason, do not change the type of a property, e.g., assign a string to a number, e.g.,
var p = new Cartesian3(1.0, 2.0, 3.0);
p.x = 'Cesium'; // Changes x to a string, slows down property access
🎨 Constructor functions should take the basic components of the class as parameters. For example, Cartesian3
takes x
, y
, and z
.
It is often convenient to construct objects from other parameters. Since JavaScript doesn't have function overloading, Cesium uses static
functions prefixed with from
to construct objects in this way. For example:
var p = Cartesian3.fromRadians(-2.007, 0.645); // Construct a Cartesian3 object using longitude and latitude
These are implemented with an optional result
parameter, which allows callers to pass in a scratch variable:
Cartesian3.fromRadians = function(longitude, latitude, height, result) {
// Compute x, y, z using longitude, latitude, height
if (!defined(result)) {
result = new Cartesian3();
}
result.x = x;
result.y = y;
result.z = z;
return result;
};
Since calling a from
constructor should not require an existing object, the function is assigned to Cartesian3.fromRadians
, not Cartesian3.prototype.fromRadians
.
Functions that start with to
return a new type of object, e.g.,
Cartesian3.prototype.toString = function() {
return '(' + this.x + ', ' + this.y + ', ' + this.z + ')';
};
🎨 Fundamental math classes such as Cartesian3
, Quaternion
, Matrix4
, and JulianDate
use prototype functions sparingly. For example, Cartesian3
does not have a prototype add
function like this:
v0.add(v1, result);
Instead, this is written as
Cartesian3.add(v0, v1, result);
The only exceptions are
clone
equals
equalsEpsilon
toString
These prototype functions generally delegate to the non-prototype (static) version, e.g.,
Cartesian3.equals = function(left, right) {
return (left === right) ||
((defined(left)) &&
(defined(right)) &&
(left.x === right.x) &&
(left.y === right.y) &&
(left.z === right.z));
};
Cartesian3.prototype.equals = function(right) {
return Cartesian3.equals(this, right);
};
The prototype versions have the benefit of being able to be used polymorphically.
To create a static constant related to a class, use freezeObject
:
Cartesian3.ZERO = freezeObject(new Cartesian3(0.0, 0.0, 0.0));
Like private properties, private functions start with an _
. In practice, these are rarely used. Instead, for better encapsulation, a file-scoped function that takes this
as the first parameter is used. For example,
Cesium3DTileset.prototype.update = function(frameState) {
this._processTiles(frameState);
// ...
};
Cesium3DTileset.prototype._processTiles(tiles3D, frameState) {
var tiles = this._processingQueue;
var length = tiles.length;
for (var i = length - 1; i >= 0; --i) {
tiles[i].process(tiles3D, frameState);
}
}
is better written as
Cesium3DTileset.prototype.update = function(frameState) {
processTiles(this, frameState);
// ...
};
function processTiles(tiles3D, frameState) {
var tiles = tiles3D._processingQueue;
var length = tiles.length;
for (var i = length - 1; i >= 0; --i) {
tiles[i].process(tiles3D, frameState);
}
}
Public properties that can be read or written without extra processing can simply be assigned in the constructor function, e.g.,
function Model(options) {
this.show = defaultValue(options.show, true);
};
Read-only properties can be created with a private property and a getter using Cesium's defineProperties
function, e.g.,
function Cesium3DTileset(options) {
this._url = options.url;
};
defineProperties(Cesium3DTileset.prototype, {
url : {
get : function() {
return this._url;
}
}
});
Getters can perform any needed computation to return the property, but the performance expectation is that they execute quickly.
Setters can also perform computation before assigning to a private property, set a flag to delay computation, or both, for example:
defineProperties(UniformState.prototype, {
viewport : {
get : function() {
return this._viewport;
},
set : function(viewport) {
if (!BoundingRectangle.equals(viewport, this._viewport)) {
BoundingRectangle.clone(viewport, this._viewport);
var v = this._viewport;
var vc = this._viewportCartesian4;
vc.x = v.x;
vc.y = v.y;
vc.z = v.width;
vc.w = v.height;
this._viewportDirty = true;
}
}
}
});
- 🚤 Calling the getter/setter function is slower than direct property access so functions internal to a class can use the private property directly when appropriate.
When the overhead of getter/setter functions is prohibitive or reference-type semantics are desired, e.g., the ability to pass a property as a result
parameter so its properties can be modified, consider combining a public property with a private shadowed property, e.g.,
function Model(options) {
this.modelMatrix = Matrix4.clone(defaultValue(options.modelMatrix, Matrix4.IDENTITY));
this._modelMatrix = Matrix4.clone(this.modelMatrix);
};
Model.prototype.update = function(frameState) {
if (!Matrix4.equals(this._modelMatrix, this.modelMatrix)) {
// clone() is a deep copy. Not this._modelMatrix = this._modelMatrix
Matrix4.clone(this.modelMatrix, this._modelMatrix);
// Do slow operations that need to happen when the model matrix changes
}
};
It is convenient for the constructor function to be at the top of the file even if it requires that helper functions rely on hoisting, for example, Cesium3DTileset.js
,
function loadTilesJson(tileset, tilesJson, done) {
// ...
}
function Cesium3DTileset(options) {
// ...
loadTilesJson(this, options.url, function(data) {
// ...
});
};
is better written as
function Cesium3DTileset(options) {
// ...
loadTilesJson(this, options.url, function(data) {
// ...
});
};
function loadTilesJson(tileset, tilesJson, done) {
// ...
}
even though it relies on implicitly hoisting the loadTilesJson
function to the top of the file.
- 🏠 Make a class or function part of the Cesium API only if it will likely be useful to end users; avoid making an implementation detail part of the public API. When something is public, it makes the Cesium API bigger and harder to learn, is harder to change later, and requires more documentation work.
- 🎨 Put new classes and functions in the right part of the Cesium stack (directory). From the bottom up:
Source/Core
- Number crunching. Pure math such asCartesian3
. Pure geometry such asCylinderGeometry
. Fundamental algorithms such asmergeSort
. Request helper functions such asloadArrayBuffer
.Source/Renderer
- WebGL abstractions such asShaderProgram
and WebGL-specific utilities such asShaderCache
. Identifiers in this directory are not part of the public Cesium API.Source/Scene
- The graphics engine, including primitives such as Model. Code in this directory often depends onRenderer
.Source/DataSources
- Entity API, such asEntity
, and data sources such asCzmlDataSource
.Source/Widgets
- Widgets such as the main CesiumViewer
.
It is usually obvious what directory a file belongs in. When it isn't, the decision is usually between Core
and another directory. Put the file in Core
if it is pure number crunching or a utility that is expected to be generally useful to Cesium, e.g., Matrix4
belongs in Core
since many parts of the Cesium stack use 4x4 matrices; on the other hand, BoundingSphereState
is in DataSources
because it is specific to data sources.
Modules (files) should only reference modules in the same level or a lower level of the stack. For example, a module in Scene
can use modules in Scene
, Renderer
, and Core
, but not in DataSources
or Widgets
.
- Modules in
define
statements should be in alphabetical order. This can be done automatically withnpm run sortRequires
, see the Build Guide. For example, the modules required byScene/ModelAnimation.js
are:
define([
'../Core/defaultValue',
'../Core/defineProperties',
'../Core/Event',
'../Core/JulianDate',
'./ModelAnimationLoop',
'./ModelAnimationState'
], function(
defaultValue,
defineProperties,
Event,
JulianDate,
ModelAnimationLoop,
ModelAnimationState) { /* ... */ });
- WebGL resources need to be explicitly deleted so classes that contain them (and classes that contain these classes, and so on) have
destroy
andisDestroyed
functions, e.g.,
var primitive = new Primitive(/* ... */);
expect(content.isDestroyed()).toEqual(false);
primitive.destroy();
expect(content.isDestroyed()).toEqual(true);
A destroy
function is implemented with Cesium's destroyObject
function, e.g.,
SkyBox.prototype.destroy = function() {
this._vertexArray = this._vertexArray && this._vertexArray.destroy();
return destroyObject(this);
};
- Only
destroy
objects that you create; external objects given to a class should be destroyed by their owner, not the class.
From release to release, we strive to keep the public Cesium API stable but also maintain mobility for speedy development and to take the API in the right direction. As such, we sparingly deprecate and then remove or replace parts of the public API.
A @private
API is considered a Cesium implementation detail and can be broken immediately without deprecation.
A public identifier (class, function, property) should be deprecated before being removed. To do so:
- Decide on which future version the deprecated API should be removed. This is on a case-by-case basis depending on how badly it impacts users and Cesium development. Most deprecated APIs will removed in 1-3 releases. This can be discussed in the pull request if needed.
- Use
deprecationWarning
to warn users that the API is deprecated and what proactive changes they can take, e.g.,
function Foo() {
deprecationWarning('Foo', 'Foo was deprecated in Cesium 1.01. It will be removed in 1.03. Use newFoo instead.');
// ...
}
- Add the
@deprecated
doc tag. - Remove all use of the deprecated API inside Cesium except for unit tests that specifically test the deprecated API.
- Mention the deprecation in the
Deprecated
section ofCHANGES.md
. Include what Cesium version it will be removed in. - Create an issue to remove the API with the appropriate
remove in [version]
label.
🏠 Cesium uses third-party libraries sparingly. If you want to add a new one, please start a thread on the Cesium forum (example discussion). The library should
- Have a compatible license such as MIT, BSD, or Apache 2.0.
- Provide capabilities that Cesium truly needs and that the team doesn't have the time and/or expertise to develop.
- Be lightweight, tested, maintained, and reasonably widely used.
- Not pollute the global namespace.
- Provide enough value to justify adding a third-party library whose integration needs to be maintained and has the potential to slightly count against Cesium when some users evaluate it (generally, fewer third-parties is better).
- GLSL files end with
.glsl
and are in the Shaders directory. - Files for vertex shaders have a
VS
suffix; fragment shaders have anFS
suffix. For example:BillboardCollectionVS.glsl
andBillboardCollectionFS.glsl
. - Generally, identifiers, such as functions and variables, use
camelCase
. - Cesium built-in identifiers start with
czm_
, for example,czm_material
. Files have the same name without theczm_
prefix, e.g.,material.glsl
. - Varyings start with
v_
, e.g.,
varying vec2 v_textureCoordinates;
- Uniforms start with
u_
, e.g.,
uniform sampler2D u_atlas;
- An
EC
suffix indicates the point or vector is in eye coordinates, e.g.,
varying vec3 v_positionEC;
// ...
v_positionEC = (czm_modelViewRelativeToEye * p).xyz;
- When GPU RTE is used,
High
andLow
suffixes define the high and low bits, respectively, e.g.,
attribute vec3 position3DHigh;
attribute vec3 position3DLow;
- 2D texture coordinates are
s
andt
, notu
andv
, e.g.,
attribute vec2 st;
- Use the same formatting as JavaScript, except put
{
on a new line, e.g.,
struct czm_ray
{
vec3 origin;
vec3 direction;
};
- 🚤 Compute expensive values as infrequently as possible, e.g., prefer computing a value in JavaScript and passing it in a uniform instead of redundantly computing the same value per-vertex. Likewise, prefer to compute a value per-vertex and pass a varying, instead of computing per-fragment when possible.
- 🚤 Use
discard
sparingly since it disables early-z GPU optimizations.
See Section 4.1 to 4.3 of Getting Serious with JavaScript by Cesium contributors Matthew Amato and Kevin Ring in WebGL Insights for deeper coverage of modules and performance.
Watch From Console to Chrome by Lilli Thompson for even deeper performance coverage.