If you do not preload media, the browser downloads each file the first time a trial needs it. For an image that means the stimulus appears some unknown number of milliseconds after the trial started, so your reaction times are contaminated by network latency and any stimulus_duration or trial_duration you set is measured against a screen that may still be blank. For audio and video it is worse: the player needs the whole file (or at least a large enough buffer) before playback can begin, so a participant on a slow connection sees a frozen screen between trials.
Preloading moves all of that waiting to one place, before the trials begin, and tells you whether it worked. This article covers how the jsPsych 8 preload plugin works, the failure modes people run into with large stimulus sets, and a block-by-block strategy that keeps waits short on slow connections.
jsPsychPreload worksThe simplest setup is a single preload trial at the start of the timeline with auto_preload: true. jsPsych scans the timeline you pass to jsPsych.run() and collects every file used in a media parameter of a standard plugin (for example the stimulus of jsPsychImageKeyboardResponse, jsPsychAudioKeyboardResponse or jsPsychVideoKeyboardResponse).
const preload = {
type: jsPsychPreload,
auto_preload: true,
show_progress_bar: true,
message: "<p>Loading the experiment, please wait...</p>",
};
timeline.push(preload);
Automatic detection only works when the file path is a plain string in the trial object. According to the official documentation it does not detect files in these cases:
stimulus: () => "audio/" + pickSound().timeline_variables through jsPsych.timelineVariable("stimulus").stimulus: "<img src='img/face1.png'>" inside jsPsychHtmlKeyboardResponse or jsPsychHtmlButtonResponse.In those cases list the files yourself with the images, audio and video arrays, or pass the relevant trials or timelines in trials and let the plugin extract what it can from them:
const stimuli = [
{ image: "faces/f01.webp", sound: "tones/high.mp3" },
{ image: "faces/f02.webp", sound: "tones/low.mp3" },
];
const preload = {
type: jsPsychPreload,
images: stimuli.map((s) => s.image),
audio: stimuli.map((s) => s.sound),
};
Two threads in the jsPsych discussion forum sum up what goes wrong when stimulus sets get large. In discussion #739 a researcher preloading several hundred audio files found that the trial failed or hung on Safari and Edge, while Chrome coped. In discussion #2306 video stimuli caused one to two minute gaps between trials because the files had not been preloaded and had to be fetched on demand.
Two parameters decide what happens when loading does not go smoothly:
max_load_time (default null): the number of milliseconds to wait for all files. With the default there is no limit, so a stalled download leaves the participant staring at a progress bar forever. Set a realistic value based on the total size of the batch.continue_after_error (default false): with the default, a failed file or a timeout stops the experiment and shows error_message ("The experiment failed to load." unless you change it). Set it to true if you would rather let the experiment carry on and handle the missing files yourself.The plugin records the outcome in its data row: success (boolean), timeout (boolean) and the arrays failed_images, failed_audio and failed_video. Those columns end up in your dataset, so you can always check afterwards which participants ran with incomplete media.
Loading 400 audio files up front means the slowest connection in your sample sets the waiting time for the first screen, and it also pushes hundreds of files into the browser cache at once. A more robust pattern is one preload trial at the start of each block, containing only the files that block needs. Each wait is short, the progress bar gives participants something to look at, and a failure is caught close to where it matters.
const jsPsych = initJsPsych();
const blocks = [
[
{ image: "faces/f01.webp", sound: "tones/high.mp3" },
{ image: "faces/f02.webp", sound: "tones/low.mp3" },
],
[
{ image: "faces/f03.webp", sound: "tones/high.mp3" },
{ image: "faces/f04.webp", sound: "tones/low.mp3" },
],
];
function preloadFor(block, index) {
return {
type: jsPsychPreload,
images: block.map((s) => s.image),
audio: block.map((s) => s.sound),
message: `<p>Loading block ${index + 1} of ${blocks.length}...</p>`,
show_progress_bar: true,
max_load_time: 60000,
continue_after_error: true,
on_error: (file) => console.warn("Failed to load", file),
};
}
const trial = {
type: jsPsychImageKeyboardResponse,
stimulus: jsPsych.timelineVariable("image"),
choices: ["f", "j"],
trial_duration: 1500,
};
const timeline = [];
blocks.forEach((block, index) => {
timeline.push(preloadFor(block, index));
timeline.push({
timeline: [trial],
timeline_variables: block,
randomize_order: true,
});
});
jsPsych.run(timeline);
The same idea works with trials instead of explicit arrays when your stimuli are plain strings in the trial objects: pass the block's timeline to trials and the plugin preloads only what it finds there.
No preloading strategy rescues an experiment whose stimuli are too large. A rough budget: at 5 Mbps, a total of 100 MB is 800 megabits and takes about 160 seconds to download, before any retries. Many participants on mobile or shared connections will be slower than that.
jsPsychVideoKeyboardResponse and list the files in the video array of the preload trial.Check total size per block, not just per file. A block of 50 clips at 2 MB each is 100 MB, which is the 160 second figure above.
Decide in advance what a missing file means for your design. If every trial depends on its stimulus, stop cleanly rather than collecting unusable data:
const preload = {
type: jsPsychPreload,
auto_preload: true,
max_load_time: 90000,
continue_after_error: true,
show_detailed_errors: true,
on_finish: (data) => {
if (!data.success) {
jsPsych.abortExperiment(
"<p>Some files could not be loaded. Please check your connection and try again.</p>"
);
}
},
};
If the design tolerates a few missing items (for example a large pool from which each participant sees a sample), you can keep continue_after_error: true, read failed_images, failed_audio and failed_video from the preload row, and exclude those items when building the rest of the timeline. show_detailed_errors: true lists the failing paths on screen, which helps during development; on_error receives each failing file as it happens.
You can also warn participants before they start. navigator.connection.downlink reports an estimated bandwidth in Mbps, but it is a non-standard API that is not available in Safari or Firefox, so treat it as a hint and never as a gate:
const downlink = navigator.connection?.downlink;
if (downlink !== undefined && downlink < 2) {
timeline.push({
type: jsPsychHtmlButtonResponse,
stimulus: "<p>Your connection seems slow. This task needs to download about 40 MB of media. If possible, connect to Wi-Fi before continuing.</p>",
choices: ["Continue"],
});
}
On Cognition you upload stimuli in the editor's stimuli tab and reference them by file name in your code, for example "faces/f01.webp" if you uploaded it in that folder, or just "f01.webp". Files are served from storage in the region of your task. Upload limits depend on your plan: Free allows files up to 2 MB and 100 files per task, Individual 100 MB and 500 files, Team 250 MB and 10,000 files. There is no automatic transcoding, so compress and resize before uploading.
The editor's live preview uses exactly the same files the participants will receive, so you can test your preload trials, their max_load_time and the error path before publishing. Open the browser's network panel during a preview run to see the real transfer size of each block. If a loading problem throws a JavaScript exception during a run, it is recorded against that run and flagged "With errors" in the runs table, and because every trial is uploaded as it finishes, you still get the partial data collected up to that point.
jsPsychPreload trial per block, with explicit images, audio and video arrays whenever paths come from functions, timeline variables or HTML strings.max_load_time that matches the block size, and decide between stopping and continuing with continue_after_error.success, timeout and the failed_* columns when you analyse the data, and exclude runs accordingly.Ready to try it with your own stimuli? Create a free account, upload your media and run a throttled preview before recruiting a single participant.