JS Array Shift and Unshift Methods: Full Tutorial
Learn how the JavaScript shift() and unshift() methods work for adding and removing elements at the start of arrays. Covers syntax, return values, performance costs, queue implementation, and when to prefer alternatives.
While push() and pop() operate on the end of an array, shift() and unshift() do the same at the beginning. These methods are essential for queue-based patterns, priority insertion, and any workflow where new data arrives at the front. However, they carry a significant performance cost that every developer should understand before using them in production.
The shift() Method
shift() removes the first element from an array and returns it. All remaining elements shift down by one index:
const notifications = ["Welcome!", "New message", "Friend request"];
const first = notifications.shift();
console.log(first); // "Welcome!"
console.log(notifications); // ["New message", "Friend request"]Syntax
const removedElement = array.shift()How shift() Re-Indexes Elements
When shift() removes the first element, every remaining element must move one position forward:
const before = ["a", "b", "c", "d"];
// Indices: 0 1 2 3
before.shift(); // removes "a"
const after = before;
// Now: ["b", "c", "d"]
// Indices: 0 1 2 ← every index changedThis re-indexing is the reason shift() runs in O(n) time. For an array with 1 million elements, the engine must update 999,999 index positions.
Shifting from an Empty Array
Like pop(), calling shift() on an empty array returns undefined:
const empty = [];
console.log(empty.shift()); // undefined
console.log(empty.length); // 0The unshift() Method
unshift() adds one or more elements to the beginning of an array and returns the new length:
const queue = ["Bob", "Carol"];
const newLength = queue.unshift("Alice");
console.log(queue); // ["Alice", "Bob", "Carol"]
console.log(newLength); // 3Adding Multiple Elements
Pass multiple arguments to insert them all at the front, preserving their argument order:
const numbers = [4, 5, 6];
numbers.unshift(1, 2, 3);
console.log(numbers); // [1, 2, 3, 4, 5, 6]Insertion Order
unshift(1, 2, 3) inserts all three as a group. The result is [1, 2, 3, ...], not [3, 2, 1, ...]. If you call unshift() three separate times with one argument each, the order reverses: unshift(1), unshift(2), unshift(3) produces [3, 2, 1, ...].
Return Value
Like push(), unshift() returns the new length, not the array:
const arr = ["x"];
const result = arr.unshift("a", "b");
console.log(result); // 3 (the new length)
console.log(arr); // ["a", "b", "x"]shift() and unshift() at a Glance
| Property | shift() | unshift() |
|---|---|---|
| Action | Removes first element | Adds element(s) to start |
| Parameters | None | One or more values |
| Returns | Removed element | New array length |
| Mutates original | Yes | Yes |
| Time complexity | O(n) | O(n) |
| On empty array | Returns undefined | Adds normally |
Building a Queue with shift() and push()
A queue follows First-In-First-Out (FIFO) order. New items enter at the back (push) and leave from the front (shift):
class PrintQueue {
constructor() {
this.jobs = [];
}
addJob(document) {
this.jobs.push({
name: document,
addedAt: new Date().toISOString(),
});
console.log(`Queued: ${document} (${this.jobs.length} in queue)`);
}
processNext() {
if (this.jobs.length === 0) {
console.log("Queue is empty");
return null;
}
const job = this.jobs.shift();
console.log(`Printing: ${job.name}`);
return job;
}
peek() {
return this.jobs[0] || null;
}
get size() {
return this.jobs.length;
}
}
const printer = new PrintQueue();
printer.addJob("Invoice_2026.pdf"); // Queued: Invoice_2026.pdf (1 in queue)
printer.addJob("Report_Q1.docx"); // Queued: Report_Q1.docx (2 in queue)
printer.addJob("Contract_Draft.pdf"); // Queued: Contract_Draft.pdf (3 in queue)
printer.processNext(); // Printing: Invoice_2026.pdf
printer.processNext(); // Printing: Report_Q1.docx
console.log(printer.size); // 1Real-World Patterns
Recent Activity Feed
Adding new items to the top of a feed while keeping a maximum size:
class ActivityFeed {
constructor(maxItems = 50) {
this.items = [];
this.maxItems = maxItems;
}
addActivity(activity) {
this.items.unshift({
...activity,
timestamp: Date.now(),
});
// Trim oldest entries if over limit
if (this.items.length > this.maxItems) {
this.items.length = this.maxItems;
}
}
getRecent(count = 10) {
return this.items.slice(0, count);
}
}
const feed = new ActivityFeed(100);
feed.addActivity({ user: "Alice", action: "created a post" });
feed.addActivity({ user: "Bob", action: "liked a photo" });
feed.addActivity({ user: "Carol", action: "commented on a thread" });
// Most recent first
console.log(feed.getRecent(2));
// [{ user: "Carol", ... }, { user: "Bob", ... }]Priority Task Processing
Inserting high-priority items at the front of a task list:
const taskQueue = [];
function addTask(task, priority = "normal") {
const entry = { task, priority, addedAt: Date.now() };
if (priority === "urgent") {
taskQueue.unshift(entry); // Goes to front
} else {
taskQueue.push(entry); // Goes to back
}
}
function processNextTask() {
return taskQueue.shift(); // Always process from front
}
addTask("Send weekly report", "normal");
addTask("Update documentation", "normal");
addTask("Fix production crash", "urgent"); // Jumps to front
addTask("Review pull request", "normal");
console.log(processNextTask().task); // "Fix production crash"
console.log(processNextTask().task); // "Send weekly report"Sliding Window Buffer
Using push() and shift() together to maintain a fixed-size buffer:
class SlidingWindow {
constructor(windowSize) {
this.buffer = [];
this.windowSize = windowSize;
}
add(value) {
this.buffer.push(value);
if (this.buffer.length > this.windowSize) {
this.buffer.shift(); // Remove oldest when window is full
}
}
average() {
if (this.buffer.length === 0) return 0;
const sum = this.buffer.reduce((acc, val) => acc + val, 0);
return sum / this.buffer.length;
}
}
// Moving average of the last 5 temperature readings
const tempWindow = new SlidingWindow(5);
[72, 68, 75, 71, 69, 80, 82].forEach(temp => {
tempWindow.add(temp);
console.log(`Reading: ${temp}, Moving avg: ${tempWindow.average().toFixed(1)}`);
});
// Reading: 72, Moving avg: 72.0
// Reading: 68, Moving avg: 70.0
// ...
// Reading: 82, Moving avg: 75.4Performance: Why Start Operations Are Slow
The fundamental difference between end operations and start operations comes down to re-indexing:
| Operation | What happens internally | Time |
|---|---|---|
push(x) | Append at index length, increment length | O(1) |
pop() | Remove at index length-1, decrement length | O(1) |
unshift(x) | Shift every element right by 1, insert at index 0 | O(n) |
shift() | Remove index 0, shift every element left by 1 | O(n) |
// Demonstrate the performance gap
function benchmark(label, fn) {
const start = performance.now();
fn();
const elapsed = performance.now() - start;
console.log(`${label}: ${elapsed.toFixed(2)}ms`);
}
const size = 100_000;
benchmark("push 100K", () => {
const arr = [];
for (let i = 0; i < size; i++) arr.push(i);
});
// push 100K: ~5ms
benchmark("unshift 100K", () => {
const arr = [];
for (let i = 0; i < size; i++) arr.unshift(i);
});
// unshift 100K: ~2000ms+ (400x slower)Avoid unshift() in Loops
Calling unshift() inside a loop with a large array creates quadratic O(n^2) time complexity. Each call shifts all existing elements. If you need to build an array in reverse order, use push() and then call reverse() once at the end, or use Array.from() with a mapping function.
Faster Alternatives to shift()/unshift()
When performance matters, restructure your logic to avoid start operations:
// Instead of unshift() in a loop:
// Slow: O(n^2)
const slow = [];
for (let i = 0; i < 10000; i++) {
slow.unshift(i);
}
// Fast: O(n) — push then reverse
const fast = [];
for (let i = 0; i < 10000; i++) {
fast.push(i);
}
fast.reverse();
// The result is identical, but "fast" is orders of magnitude quickerFor queue patterns on large datasets, consider a linked list or a circular buffer instead of an array with shift().
Comparison: All Four Mutation Methods
Immutable Alternatives
When you need to avoid mutating the original array (React state, Redux, pure functions):
const original = ["b", "c", "d"];
// Instead of unshift("a"):
const withFront = ["a", ...original];
console.log(withFront); // ["a", "b", "c", "d"]
console.log(original); // ["b", "c", "d"] — unchanged
// Instead of shift():
const [removed, ...rest] = original;
console.log(removed); // "b"
console.log(rest); // ["c", "d"]
console.log(original); // ["b", "c", "d"] — unchangedCommon Mistakes
Confusing the return value of unshift():
// Bug: unshift returns the new length, not the array
const arr = [2, 3];
const result = arr.unshift(1);
console.log(result); // 3 (length, not the array)Using shift() to clear an array:
// Slow: O(n^2) total — avoid this pattern
const items = [1, 2, 3, 4, 5];
while (items.length > 0) {
items.shift(); // Re-indexes on every call
}
// Fast: O(1)
items.length = 0;Not realizing unshift preserves argument order:
const arr = [4, 5];
// This inserts [1, 2, 3] as a group at the front
arr.unshift(1, 2, 3);
console.log(arr); // [1, 2, 3, 4, 5] ✓
// Calling separately reverses the order
const arr2 = [4, 5];
arr2.unshift(3);
arr2.unshift(2);
arr2.unshift(1);
console.log(arr2); // [1, 2, 3, 4, 5] — same result only because we called in sequence
// But this would differ:
const arr3 = [4, 5];
arr3.unshift(1);
arr3.unshift(2);
arr3.unshift(3);
console.log(arr3); // [3, 2, 1, 4, 5] — reversed!Rune AI
Key Insights
- shift() removes from start, unshift() adds to start: Both are O(n) because they re-index every element
- Queue pattern: Combine
push()(enqueue at back) andshift()(dequeue from front) for FIFO order - Return values:
shift()returns the removed element;unshift()returns the new length - Performance awareness: Avoid
shift()/unshift()in loops over large arrays; usepush()+reverse()instead - Immutable alternatives: Use
[newItem, ...arr]andconst [first, ...rest] = arrwhen mutation is not acceptable
Frequently Asked Questions
What is the difference between shift() and pop()?
When should I use unshift() instead of push()?
Does shift() modify the original array?
How do I unshift without mutating the original array?
Why is shift() so slow for large arrays?
Conclusion
shift() and unshift() complete the quartet of basic array mutation methods alongside push() and pop(). They enable queue patterns, priority insertion, and front-of-array manipulation, but their O(n) time complexity means they should be used deliberately. For small arrays or low-frequency operations they work well. For large arrays in tight loops, restructure your code to use push()/pop() with a final reverse(), or switch to a data structure designed for efficient front operations.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.