DEV Community

RadcliffBarrett4718
RadcliffBarrett4718

Posted on

Upload-Time vs On-Demand Ecommerce Image Compression Settings (Quality Versus Bytes)

TL;DR: Compress catalogue photos at upload time when a fintech promotion service repeatedly turns a stable set of product images into short videos. Choose on-demand compression only when output dimensions are genuinely unknowable until a request arrives. Keep the original, create a small named set of derivatives, and measure quality decisions alongside bytes and latency.

Decision signal Upload-time compression On-demand compression
Catalogue changes less often than videos are generated Pick this Repeats work or needs a cache
Known placements and aspect ratios Pick this Adds little flexibility
New dimensions appear without a release Requires another derivative job Pick this
First-request latency is tightly bounded Work is already complete Requires a miss policy
Quality review must reproduce the exact asset Simple with immutable derivative IDs Every transform input must be recorded

My choice for this workload is upload-time compression. A product photo may appear in many generated promo videos, while the accepted placements are usually a short list: portrait, square, and landscape. Doing the expensive decision once makes delivery boring. Boring is good.

This is not a claim that one codec setting fits every photograph. It is an architecture choice: move the quality-versus-bytes decision into an observable ingestion job, preserve the source, and publish only derivatives that pass explicit checks.

Start there.

How should an ecommerce API choose image compression settings?

The unit of reuse decides it. A catalogue image is an asset; a generated video is an event. If one asset feeds many events, compression belongs with the asset lifecycle. The video-generation path can then read a derivative by a stable key instead of negotiating width, format, and quality while a user waits.

Picture the flow in words: source upload -> validation -> decode -> orientation and color handling -> resize -> encode candidates -> quality gate -> immutable object -> catalogue manifest -> video render. Logs follow the job ID. Metrics aggregate by derivative profile and source class. A trace connects the upload request to the published manifest.

The important boundary sits before publication. An encoder returning bytes does not mean the image is acceptable. The job still needs to confirm dimensions, byte length, decodability, and intended format. Visual quality needs a review policy too. Automated comparison can flag candidates, but thresholds should be calibrated against representative catalogue photos rather than copied from a generic example. Text on packaging, fine jewelry, fabric, and smooth gradients fail differently.

Consider one portrait profile at 1080 x 1350. A source with tiny ingredient text can satisfy the dimensions and byte ceiling yet still be a poor catalogue result. Another source with a plain background may remain clear at a much smaller output size. The API cannot infer merchandising importance from byte count alone. That is why the gate needs both machine checks and a representative human-reviewed fixture set. It is also why a rejected image must retain its source identity and reason code. Operators need to distinguish a decode failure from a quality decision without opening every file. One number cannot carry all of that meaning.

That changes the API design. Do not expose a free-form quality integer as the business contract. Expose a derivative profile such as video-portrait-v1. The profile owns dimensions, crop behavior, format policy, encoder settings, and gate thresholds. Version it when those decisions change.

Pick upload-time compression for a controlled catalogue

Use this path when placements are known, images are reused, and the team wants a reviewable release boundary. It gives operators a finite batch to inspect before a campaign starts. It also lets a failed derivative stay unpublished while the original remains available for a corrected job.

Keep three identities separate: the source asset ID, transformation specification, and resulting derivative ID. A source filename is not enough. Two uploads can share a name, and one upload can produce several valid outputs. A content digest plus a versioned profile makes the relationship explicit without pretending filenames are immutable identifiers.

type DerivativeProfile = {
  id: "video-portrait-v1" | "video-square-v1" | "video-landscape-v1";
  width: number;
  height: number;
  fit: "cover" | "contain";
  preferredFormats: Array<"avif" | "webp" | "jpeg">;
};

type PublishedDerivative = {
  sourceDigest: string;
  profileId: DerivativeProfile["id"];
  objectKey: string;
  format: "avif" | "webp" | "jpeg";
  width: number;
  height: number;
  bytes: number;
};

const profiles: DerivativeProfile[] = [
  { id: "video-portrait-v1", width: 1080, height: 1350, fit: "cover", preferredFormats: ["avif", "webp", "jpeg"] },
  { id: "video-square-v1", width: 1080, height: 1080, fit: "cover", preferredFormats: ["avif", "webp", "jpeg"] },
  { id: "video-landscape-v1", width: 1200, height: 675, fit: "cover", preferredFormats: ["avif", "webp", "jpeg"] },
];
Enter fullscreen mode Exit fullscreen mode

Those dimensions are example application choices, not universal recommendations. The right values come from actual video templates and must be tested with the actual renderer. The format list is also a policy input. MDN's image format guide is a useful compatibility reference, but the consuming decoder remains the authority for this pipeline.

Pick on-demand compression when requests define the output

On-demand processing earns its complexity when callers introduce legitimate, unbounded dimensions or when a new placement must work before a derivative backfill can finish. It can also serve as a controlled fallback during a profile migration. In each case, the cache key must represent the complete transformation, not just the source URL.

A safe mental model is request -> normalize specification -> authorize bounds -> derive cache key -> check immutable result -> transform on miss -> validate -> publish atomically -> respond. The normalization step matters. Width 800, width 0800, and a floating-point width should not create three jobs for the same output. Reject impossible dimensions and unsupported formats before decoding the source.

This option moves failure into a latency-sensitive path. A cache miss needs a deadline and a defined response: wait, return a known fallback derivative, or enqueue work and report that the asset is not ready. That is a product decision. Hiding it inside retry middleware produces ambiguous behavior and noisy load.

On-demand also needs request coalescing so simultaneous misses for one derivative do not all encode it. That is an implementation obligation, not a reason to reject the design. Put it on the decision table before choosing.

This is a real trade-off.

Implement the upload path as a measured state machine

Start with states that an operator can name: received, validated, encoding, gated, published, and rejected. State transitions should be idempotent. A retry after an interrupted upload must either find the same published derivative or resume work without exposing a partially written object.

The encoder adapter can stay generic. Its contract accepts decoded source data and a profile, then returns candidate bytes plus facts observed from decoding the result. The gate decides whether to publish. This separation makes it possible to change an encoding library without changing catalogue semantics.

type Candidate = PublishedDerivative & {
  durationMs: number;
  decoded: boolean;
  qualityScore?: number;
};

type GatePolicy = { maxBytes: number; minQualityScore?: number };

function evaluateCandidate(candidate: Candidate, policy: GatePolicy): string[] {
  const failures: string[] = [];
  if (!candidate.decoded) failures.push("output_decode_failed");
  if (candidate.bytes > policy.maxBytes) failures.push("byte_budget_exceeded");
  if (policy.minQualityScore !== undefined) {
    if (candidate.qualityScore === undefined) failures.push("quality_score_missing");
    else if (candidate.qualityScore < policy.minQualityScore) failures.push("quality_gate_failed");
  }
  return failures;
}
Enter fullscreen mode Exit fullscreen mode

The numeric policy is intentionally supplied by the application. There is no honest universal maxBytes or quality threshold for real catalogue photography. Establish it with a review set that includes difficult material in this catalogue, then store the policy version with every result. If reviewers disagree often, refine the set or split the profile instead of quietly loosening the threshold.

Observability should answer a decision, not merely prove that the job ran. Record source bytes, output bytes, dimensions, format, profile version, processing duration, outcome, and a bounded reason code. Aggregate compression ratio by profile, but keep raw asset identifiers out of metric labels; high-cardinality identifiers belong in logs and traces. Alert on sustained rejection or processing-latency changes, not on a single difficult photograph.

type CompressionEvent = {
  jobId: string;
  sourceDigest: string;
  profileId: DerivativeProfile["id"];
  sourceBytes: number;
  outputBytes?: number;
  durationMs: number;
  outcome: "published" | "rejected";
  reason?: "output_decode_failed" | "byte_budget_exceeded" | "quality_score_missing" | "quality_gate_failed";
};

function metricLabels(event: CompressionEvent) {
  return {
    profile: event.profileId,
    outcome: event.outcome,
    reason: event.reason ?? "none",
  };
}
Enter fullscreen mode Exit fullscreen mode

Test the state machine at three levels. Unit tests cover normalization and gates. Fixture tests run known photos through each profile and verify decodability, dimensions, and policy outcomes. A deployment check processes a small representative set before a new profile becomes active. Store expected decisions, not exact encoded bytes, because encoder implementations may legitimately produce different byte streams.

Rollout should be reversible. Publish video-portrait-v2 beside v1, send a limited set of assets through both, compare gate outcomes and operational distributions, then move the manifest pointer. Do not overwrite v1. Existing videos and investigations need the old identity to keep meaning the same thing.

Limits of the upfront choice

The main limitation is derivative multiplication. Upload-time processing is not a fit when each request introduces a valid new canvas size, when most derivatives are never reused, or when storage policy cannot retain the resulting set. Choose bounded on-demand compression instead in those cases, with normalized inputs, request coalescing, and a defined miss response. New video layouts still require new profiles and a backfill under the upfront model. Rare source images may need manual review. A strict byte gate can reject a visually important asset that refuses to compress cleanly, so the rejection path needs an owner and a visible queue. The operational trade-off is plain: upfront work buys predictable delivery, while on-demand work buys flexibility at request time.

The choice should be revisited if requests become less predictable than the catalogue, if derivatives multiply faster than they are reused, or if the consuming renderer changes format support. Until then, stable catalogue assets and repeated promo generation favor a small, versioned derivative set produced before request time. Keep the source. Measure the gate. Publish immutably.

References

Top comments (0)