Static Resources in Salesforce: Upload, Reference, and Use in Apex and LWC
What Are Static Resources?
A Static Resource is a file — or a zip archive of files — stored inside your Salesforce org and served
directly from Salesforce's content delivery infrastructure. JavaScript libraries, CSS files, images, fonts, JSON data
files, and entire single-page application bundles all belong here. Unlike documents stored in ContentVersion
or Attachment, Static Resources are version-controlled through deployments, reference-able in Visualforce and
LWC, and accessible in Apex at runtime via PageReference — making them the correct tool whenever you need a
file that is part of your application code rather than user data.
The key facts you must carry into every design decision:
- Maximum size per resource: 5 MB (single file or zip).
- Total static resource storage per org: 250 MB.
- Cache-Control is set by you:
Public(CDN-cached, fastest) orPrivate(per-session, for sensitive assets). - Zip archives let you bundle entire folder structures — a single zip can serve a complete third-party library with sub-directories intact.
- The API name you give the resource is how every reference to it is made — choose names that are stable and namespaced if you ship packages.
Uploading Static Resources
Through Setup UI
- Navigate to Setup → Custom Code → Static Resources.
- Click New.
- Enter a unique Name (alphanumeric, underscores only — this is the API name).
- Choose your file. If you are uploading a folder structure, zip it first.
- Set Cache Control. Use
Publicfor libraries and images that are not user-specific. UsePrivatefor anything tied to a user session. - Save.
The MIME type is inferred from the file extension. For zips, Salesforce stores the archive and lets you reference individual paths inside it using a trailing path suffix on the resource URL.
Through Metadata API / SFDX
For version-controlled projects this is the correct approach. The directory structure is:
force-app/
└── main/
└── default/
└── staticresources/
├── chartjs.resource ← the actual file or zip
├── chartjs.resource-meta.xml ← metadata descriptor
├── myStyles.resource
└── myStyles.resource-meta.xml
The -meta.xml descriptor for a single JS file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
<cacheControl>Public</cacheControl>
<contentType>application/javascript</contentType>
<description>Chart.js 4.4 minified bundle</description>
</StaticResource>
For a zip archive the contentType is application/zip. Deploy with:
sf project deploy start --source-dir force-app/main/default/staticresources
Referencing Static Resources in Apex
Apex gives you two ways to reach a static resource at runtime: the PageReference approach for getting a
URL string, and direct content retrieval via StaticResourceCallout patterns. The most common use case is
generating a URL to pass into a response or to embed in dynamic Visualforce output.
Getting the Resource URL
public class StaticResourceHelper {
/**
* Returns the absolute URL for a top-level static resource.
* Suitable for single-file resources (JS, CSS, image).
*/
public static String getResourceUrl(String resourceName) {
PageReference ref = PageReference.forResource(resourceName);
// getUrl() returns a relative path: /resource/TIMESTAMP/NAME
return ref.getUrl();
}
/**
* Returns the URL for a specific file inside a zip resource.
* pathInsideZip must NOT start with a leading slash.
* Example: getZipFileUrl('chartjsBundle', 'chart.umd.min.js')
*/
public static String getZipFileUrl(String resourceName, String pathInsideZip) {
PageReference ref = PageReference.forResource(resourceName, pathInsideZip);
return ref.getUrl();
}
}
PageReference.forResource(String name) and its two-argument variant
PageReference.forResource(String name, String path) are the canonical way to resolve static resource
URLs in Apex. The URL they return includes a cache-busting version timestamp that Salesforce manages automatically on
each deployment — you never hard-code this timestamp.
Reading Static Resource Content in Apex
There are scenarios where you need to read the content of a static resource at runtime — for example, loading
a JSON configuration file or reading an XML template. Query the StaticResource sObject, which exposes the
file body as a Blob.
public class StaticResourceReader {
/**
* Reads a text-based static resource (JSON, XML, CSV, etc.)
* and returns its content as a String.
* Throws QueryException if the resource does not exist.
*/
public static String readTextResource(String resourceName) {
StaticResource sr = [
SELECT Id, Body
FROM StaticResource
WHERE Name = :resourceName
LIMIT 1
];
return sr.Body.toString();
}
/**
* Parses a JSON static resource directly into a Map.
* The resource must contain a valid JSON object at its root.
*/
public static Map readJsonResource(String resourceName) {
String rawJson = readTextResource(resourceName);
return (Map) JSON.deserializeUntyped(rawJson);
}
}
The Body field on StaticResource returns a Blob. Calling
.toString() on it gives you the UTF-8 string content. This pattern is ideal for feature-flag JSON files
or lookup tables that change infrequently and do not belong in Custom Metadata (because they are too large or
structured as arbitrary JSON).
Practical Example — Loading a Country Code Map
// Static resource Name: 'CountryCodeMap'
// Content: {"US":"United States","GB":"Great Britain","IN":"India", ...}
public class CountryCodeService {
private static Map codeMap;
private static Map getCodeMap() {
if (codeMap == null) {
codeMap = StaticResourceReader.readJsonResource('CountryCodeMap');
}
return codeMap;
}
public static String getCountryName(String isoCode) {
Object name = getCodeMap().get(isoCode.toUpperCase());
return name != null ? (String) name : 'Unknown';
}
}
The lazy-loaded static variable means the SOQL fires once per transaction regardless of how many times
getCountryName is called — an important governor limit consideration.
Referencing Static Resources in Lightning Web Components
LWC uses the @salesforce/resourceUrl scoped import to get the URL of a static resource at
component load time. The platform resolves the versioned URL for you — no Apex call required.
Single-File Resource
// myImageCard.js
import { LightningElement } from 'lwc';
import myLogo from '@salesforce/resourceUrl/CloudEzeeLogo';
export default class MyImageCard extends LightningElement {
logoUrl = myLogo;
}
<!-- myImageCard.html -->
<template>
<img src={logoUrl} alt="CloudEzee Logo" />
</template>
The imported value is a fully-qualified URL string. Assign it directly to a property and bind it in template markup.
Zip Resource — Accessing Nested Files
When your static resource is a zip, the import gives you the base URL. Append the internal file path using string
concatenation. Suppose you have a zip named chartjsBundle with this structure:
chartjsBundle.zip
├── chart.umd.min.js
├── chart.umd.min.js.map
└── LICENSE
// chartWidget.js
import { LightningElement, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import chartjsBase from '@salesforce/resourceUrl/chartjsBundle';
export default class ChartWidget extends LightningElement {
@track chartInitialized = false;
@track error;
connectedCallback() {
// loadScript returns a Promise; handle both resolve and reject
loadScript(this, chartjsBase + '/chart.umd.min.js')
.then(() => {
this.chartInitialized = true;
this.initializeChart();
})
.catch(err => {
this.error = err.message;
console.error('Chart.js failed to load:', err);
});
}
initializeChart() {
const canvas = this.template.querySelector('canvas');
// Chart is now available as a global from the loaded script
// eslint-disable-next-line no-undef
new Chart(canvas, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [{
label: 'Monthly Revenue',
data: [12000, 19000, 14000, 22000, 17500],
backgroundColor: 'rgba(21, 137, 238, 0.7)'
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
}
<!-- chartWidget.html -->
<template>
<template if:true={error}>
<p class="slds-text-color_error">{error}</p>
</template>
<template if:true={chartInitialized}>
<div style="height: 300px;">
<canvas></canvas>
</div>
</template>
</template>
loadScript and loadStyle
The lightning/platformResourceLoader module exposes two functions:
loadScript(component, url)— injects a<script>tag into the DOM and returns aPromisethat resolves when the script executes.loadStyle(component, url)— injects a<link rel="stylesheet">tag and returns aPromise.
Both accept the component instance as their first argument (always this) so the platform can scope and
track the injected assets per component. Both are idempotent within a page — if the same URL is requested by multiple
component instances, the script/style is only loaded once.
Loading Multiple Resources in Order
Some libraries depend on each other and must load sequentially. Use Promise chaining:
import { LightningElement } from 'lwc';
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import jqueryBase from '@salesforce/resourceUrl/jqueryBundle';
import selectizeBase from '@salesforce/resourceUrl/selectizeBundle';
export default class SelectizeWidget extends LightningElement {
connectedCallback() {
loadScript(this, jqueryBase + '/jquery.min.js')
.then(() => loadScript(this, selectizeBase + '/selectize.min.js'))
.then(() => loadStyle(this, selectizeBase + '/selectize.default.css'))
.then(() => this.initSelectize())
.catch(err => console.error('Resource load failed:', err));
}
initSelectize() {
// jQuery and Selectize are now available globally
}
}
Each .then() waits for the previous load to complete before beginning the next. This guarantees
Selectize finds jQuery already defined in the global scope when it executes.
Using Static Resources in Visualforce
For completeness — Visualforce pages reference static resources through the $Resource global merge field.
<!-- Single file resource -->
<apex:includeScript value="{!$Resource.myScript}"/>
<apex:stylesheet value="{!$Resource.myStyles}"/>
<!-- File inside a zip -->
<apex:includeScript value="{!URLFOR($Resource.chartjsBundle, 'chart.umd.min.js')}"/>
<apex:image url="{!URLFOR($Resource.imageBundle, 'icons/logo.png')}" />
URLFOR(resource, path) is the Visualforce equivalent of appending a path to a zip resource URL. The
pattern maps directly to PageReference.forResource(name, path) in Apex.
Namespaced Static Resources in Managed Packages
If your component ships inside a managed package with namespace mypkg, the import syntax changes:
import myAsset from '@salesforce/resourceUrl/mypkg__myResourceName';
In subscriber orgs the resource name is prefixed with the namespace and double underscore. In the packaging org itself you reference it without the prefix. Structure your code so the import lives in one place to make namespace swapping a single-line change.
Apex Test Coverage for Static Resource Reads
SOQL against StaticResource works in test context only if the resource exists in the org. For unit tests
you should stub the data layer behind a thin interface so the test does not depend on org state.
@IsTest
private class CountryCodeServiceTest {
@IsTest
static void testGetCountryNameReturnsLabel() {
// StaticResource 'CountryCodeMap' must exist in the org for this
// integration-style test to pass. Use it in full-copy sandboxes.
// For isolated unit tests, mock StaticResourceReader.readJsonResource.
Test.startTest();
String result = CountryCodeService.getCountryName('US');
Test.stopTest();
System.assertEquals('United States', result,
'US ISO code should resolve to United States');
}
@IsTest
static void testGetCountryNameUnknownCode() {
Test.startTest();
String result = CountryCodeService.getCountryName('ZZ');
Test.stopTest();
System.assertEquals('Unknown', result,
'Unrecognised code should return Unknown');
}
}
For unit tests that must run without org data, abstract the SOQL behind a @TestVisible virtual method
or a selector pattern so a mock can inject a Blob directly — the same approach used for any sObject
dependency.
Static Resources vs. Other File Storage Options
Knowing when not to use Static Resources is just as important as knowing how to use them.
- Static Resource — application assets that are part of your deployment: libraries, icons, font files, configuration JSON. Changed via deployment, not by users.
- ContentVersion / Files — user-uploaded documents, attachments, shared files. Changed at runtime by users. Full version history, sharing rules, and visibility controls.
- Custom Metadata Types — small, structured configuration data queryable in SOQL, deployable, and available in formula fields. Better than a JSON static resource for tabular config.
- Custom Labels — translatable string values. Never put translatable UI copy into a Static Resource.
Common Mistakes and How to Avoid Them
-
Hard-coding the versioned URL path. Never copy the
/resource/TIMESTAMP/NAMEURL from your browser and paste it into code. The timestamp changes on every deployment and will break your reference. Always use@salesforce/resourceUrl/in LWC orPageReference.forResource()in Apex. -
Forgetting the path separator on zip resources. Use a forward slash with no leading slash:
chartjsBase + '/chart.umd.min.js'. A double slash or missing slash produces a 404. -
Calling
loadScriptinsiderenderedCallbackwithout a guard.renderedCallbackfires every time the component re-renders. Add a boolean flag (this.scriptsLoaded) and check it before callingloadScriptto prevent repeated injections. - Exceeding the 5 MB limit with unminified libraries. Always upload the production/minified build. Tree-shake or bundle with Rollup/esbuild before zipping if you only need a subset of a library.
-
Using
Privatecache control on public libraries. Setting cache control toPrivateon a CDN-safe asset like Chart.js prevents edge-caching and increases load time for every user. ReservePrivatefor assets that contain org-specific or user-specific information.
Full Working Example — Image Gallery LWC with Zip Resource
This example bundles multiple images in a zip, references each by constructed URL, and renders a responsive gallery.
// Zip structure:
// productImages.zip
// ├── hero.jpg
// ├── thumb1.jpg
// ├── thumb2.jpg
// └── thumb3.jpg
// imageGallery.js
import { LightningElement } from 'lwc';
import productImagesBase from '@salesforce/resourceUrl/productImages';
export default class ImageGallery extends LightningElement {
images = [
{ id: 1, src: productImagesBase + '/thumb1.jpg', alt: 'Product shot 1' },
{ id: 2, src: productImagesBase + '/thumb2.jpg', alt: 'Product shot 2' },
{ id: 3, src: productImagesBase + '/thumb3.jpg', alt: 'Product shot 3' }
];
heroSrc = productImagesBase + '/hero.jpg';
}
<!-- imageGallery.html -->
<template>
<figure>
<img src={heroSrc} alt="Hero product" style="width:100%;" />
</figure>
<div class="slds-grid slds-wrap slds-gutters_small">
<template for:each={images} for:item="img">
<div key={img.id} class="slds-col slds-size_1-of-3">
<img src={img.src} alt={img.alt} style="width:100%;" />
</div>
</template>
</div>
</template>
Summary
Static Resources are Salesforce's deployment-managed file store for application assets. Upload single files for
standalone scripts, images, or stylesheets; upload zip archives when a library ships with subdirectories or you need
to bundle related assets together. In LWC, always use the @salesforce/resourceUrl/ import — it gives
you a versioned URL the platform keeps current on every deploy. Load scripts and styles through
lightning/platformResourceLoader so the platform manages injection lifecycle; chain promises when
libraries have dependencies. In Apex, use PageReference.forResource() for URL generation and query the
StaticResource sObject when you need to read file content at runtime — then cache aggressively inside
the transaction. Keep Cache Control set to Public for all library and image assets to benefit from CDN
caching, and never hard-code versioned URL paths anywhere in your code.
Frequently Asked Questions
What is the difference between Cache-Control Public and Private for a static resource, and when should each be used?
Public cache control allows Salesforce's CDN to cache the resource across sessions, making it significantly faster to load for all users. Private cache control serves the resource per-session, bypassing the CDN. Use Public for JavaScript libraries, CSS, and images that are identical for every user; use Private only for assets that are session-specific or contain user-sensitive data.
How do you reference a specific file inside a zip static resource rather than the zip itself?
When a static resource is a zip archive, Salesforce lets you append a path suffix to the resource URL to target a specific file within the archive — for example, appending /subfolder/library.min.js to the base resource URL. In LWC, the @salesforce/resourceUrl import gives you the base URL, and you concatenate the internal path manually. This is how entire third-party libraries with nested directories (like Chart.js or D3) can be bundled and selectively referenced.
Can the API name of a static resource be changed after it has been deployed to production?
Renaming a static resource's API name requires creating a new resource with the new name and deleting the old one, because every reference in Apex, Visualforce, and LWC is hard-coded to the original API name. Any component or class referencing the old name will break at runtime. This is why choosing a stable, well-namespaced name at creation time is critical, especially for managed packages where the namespace prefix is prepended automatically.
What happens if a static resource zip exceeds 5 MB after compression?
Salesforce enforces a hard 5 MB limit per individual static resource, and the zip file itself — not its uncompressed contents — must fall under that threshold. If a library exceeds the limit, the common approach is to split it across multiple static resources (e.g., separating core and plugin bundles) and reference them independently. Alternatively, loading large third-party assets from a trusted external CDN via a remote site setting is an option, though it introduces an external dependency and requires CSP configuration.