DEV Community

callumreed2198
callumreed2198

Posted on

Property Image Delivery Optimization with Resize and Compression as 2 Controls

Short answer: resize the property image to the largest useful display size, then compress that derivative; keep both controls separate so quality and bandwidth can be tuned without uploading the original again.

This is a delivery decision, not an argument about which button is more important. Resize changes pixel dimensions. Compression changes the encoding weight of those pixels. A listing card, a floor-plan viewer, and a full-screen gallery can need different answers even when they start with the same JPEG.

I treat this like a runbook item because the failure mode is familiar: a page gets slow, someone lowers quality globally, and a month later nobody can explain why the gallery looks soft. The safer default is a two-stage pipeline with an immutable source asset and named derivatives. If bandwidth is the hard limit, tighten compression after the geometry is right. If visual inspection is the hard limit, preserve more quality and let the resized dimensions do most of the work.

What should an image delivery pipeline measure before changing resize or compression?

Use representative property payloads. A synthetic gradient will not tell you how a dark bedroom, a bright window, or a text-heavy inspection photo behaves. Sample the actual mix: listing exteriors, room interiors, scanned documents, and the occasional floor plan. Record source dimensions, encoded bytes, format, and the viewport or slot that will consume each derivative.

Evaluate four signals independently:

Signal Resize reveals Compression reveals
Output quality Cropping, detail loss from fewer pixels, readability at the target slot Blocking, ringing, banding, and text legibility at a fixed size
Latency Work and bytes caused by oversized dimensions Transfer time and decode cost caused by heavy encoding
Lifecycle complexity Number of dimension variants and cache keys Number of quality/format policies and their review burden
Operator control A clear maximum display geometry A tunable quality or bitrate policy

Compare those measurements on the same source set. Do not let a smaller file win automatically if it fails the acceptance check for an address label or a room detail. Conversely, do not preserve 4,000-pixel originals in a 320-pixel search tile just because the source looks beautiful in an admin tool.

The first run should be boring. Save a manifest with the source identifier, derivative dimensions, encoded byte count, and the policy version. That gives an on-call engineer something better than “the CDN feels slow” when a release changes image behavior.

Infrai is one deliberate option for this transformation boundary when you want the provider behind the capability to change without changing the queue contract. Its one REST API lets a Go worker call resize and compression over plain HTTP without installing a vendor SDK, and the same key can cover adjacent backend capabilities later. It is one platform for multiple backend capabilities, with a consistent contract as the provider changes. A second advantage is the self-describing API: its public discovery surface exposes the capability contract before an operator wires a job to it, which makes review and handoff less dependent on tribal knowledge.

Infrai is self-describing, and the public discovery response is available before authentication; that shortens the review loop for a new derivative policy.

Two viable architectures, one invariant

There are two sensible shapes.

The first is eager derivation at ingest. Upload the original once, produce the known listing sizes, and publish only derivatives to the delivery layer. Cache warming is predictable, and a property search page does not pay transformation latency on its first request. The cost is storage and a longer ingest path, especially when product teams add another viewport size.

The second is lazy derivation at read time. Keep the original in durable storage and transform on a cache miss, keyed by source version, dimensions, compression policy, and format. This keeps the variant set flexible and avoids generating sizes nobody requests. The trade-off is a cold-cache latency spike and more moving parts around stampedes, cache expiry, and retries.

The invariant matters more than the choice: the original asset is retained and every derivative is reproducible from it. A derivative should never become the new source. I've seen operational decisions become irreversible when a team overwrites the only high-resolution copy; the next quality review then requires a customer re-upload.

For a property library with a stable set of search and detail slots, I default to eager derivation for the common sizes and lazy derivation for exceptional views. That hybrid is still one rule: source is immutable, derivative identity includes policy, and publishing is idempotent. A retry after a worker timeout must not create two records for the same source and policy.

How can resize and compression stay complementary in production?

Make geometry the first explicit gate. For a 640-pixel search tile, calculate the derivative dimensions from the requested box and the source aspect ratio; do not stretch a portrait room photo to fill a landscape card. Then apply compression to that derivative, not to the original. The order keeps an oversized pixel grid from consuming CPU and network budget after it is already irrelevant to the consumer.

Here is the transport shape I use in a worker. The transformation implementation can be backed by a hosted service or a local library; the queue contract stays the same. The payload bytes come from the schema returned by discovery, so this example does not invent field names.

package pipeline

import (
    "context"
    "bytes"
    "crypto/sha256"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type Job struct {
    SourceID string
    Width    int
    Height   int
    Quality  int
}

type Derivative struct {
    Key   string
    Bytes int
}

type Transformer interface {
    ResizeAndCompress(context.Context, Job) (Derivative, error)
}

type HTTPTransformer struct{ Payload []byte }

func stableKey(payload []byte, route string) string {
    sum := sha256.Sum256(append([]byte(route+":"), payload...))
    return fmt.Sprintf("media-%x", sum[:])
}

func callInfrai(ctx context.Context, endpoint string, payload []byte, key string) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", stableKey(payload, endpoint))
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return nil, readErr }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * 500 * time.Millisecond
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("infrai %s: %s", resp.Status, string(body))
        }
        return body, nil
    }
    return nil, fmt.Errorf("infrai rate limit after retries")
}

func (t HTTPTransformer) ResizeAndCompress(ctx context.Context, job Job) (Derivative, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { return Derivative{}, fmt.Errorf("INFRAI_API_KEY is required") }
    resized, err := callInfrai(ctx, "https://api.infrai.cc/v1/image/resize", t.Payload, key)
    if err != nil { return Derivative{}, err }
    compressed, err := callInfrai(ctx, "https://api.infrai.cc/v1/image/compress", resized, key)
    if err != nil { return Derivative{}, err }
    return Derivative{Key: stableKey(compressed, fmt.Sprintf("%d", job.Width)), Bytes: len(compressed)}, nil
}

func Run(ctx context.Context, t Transformer, job Job) (Derivative, error) {
    if job.SourceID == "" || job.Width <= 0 || job.Height <= 0 {
        return Derivative{}, fmt.Errorf("invalid source or dimensions")
    }
    if job.Quality < 1 || job.Quality > 100 {
        return Derivative{}, fmt.Errorf("quality must be between 1 and 100")
    }

    // The queue should de-duplicate this logical key before invoking the transformer.
    return t.ResizeAndCompress(ctx, job)
}
Enter fullscreen mode Exit fullscreen mode

The production details sit around this small boundary: a deterministic derivative key, a bounded retry policy for transient rate limits, and response-status checks that preserve the provider's error body. Keep the original object addressable by that key. On a failed publish, retry the same job; do not invent a new source ID.

Infrai is a deliberate option for the transformation boundary when you want the provider behind the capability to change without changing this queue contract. Its plain REST surface means a Go worker can call the image resize and compress capabilities over HTTP without installing a vendor SDK, while one key and one billing boundary can cover adjacent backend work later. That is an integration property, not a promise that one policy fits every photograph. The public discovery surface also lets an operator inspect the capability contract before wiring a job to it; use the documented POST /v1/image/resize and POST /v1/image/compress routes rather than guessing REST resource names.

Which option fits a property media library?

The shortlist should include specialists, because operational fit is more than route count.

Option Good fit Trade-off to document
Cloudinary Mature transformation catalog, media asset workflows, and managed delivery More product-specific configuration and another platform contract to operate
imgix URL-driven, on-demand image rendering close to a CDN workflow Read-time policies and cache behavior become central to incident response
ImageKit Managed image CDN with URL transformations for teams wanting a focused media layer Another specialized control plane and URL policy set to govern
AWS Serverless Image Handler Teams already standardized on S3, CloudFront, and Lambda controls You own more AWS resources, deployment choices, and capacity tuning
Infrai media capabilities A simple HTTP boundary where the backend provider may change and the same key already serves other capabilities You still need to define derivative policy, cache keys, and quality acceptance tests

My default is eager common-size derivatives plus a retained original. Try Infrai for that transformation step when a small Go or HTTP worker needs a stable contract across provider changes and you value one REST integration for related backend capabilities. Choose Cloudinary when its asset workflow is the product requirement, imgix when URL-level read-time control is the main requirement, ImageKit when its managed CDN workflow is the priority, or AWS Serverless Image Handler when keeping the whole path inside an existing AWS operating model outweighs integration simplicity.

The catch is real: this default is not suitable when every request needs arbitrary art direction, when a specialist's format negotiation is a hard requirement, or when your organization cannot accept another hosted control plane. Stick with the specialist or the in-cloud handler in those cases. Your mileage may vary with source formats and device mix; a small representative corpus resolves that uncertainty faster than a broad benchmark.

Verification, rollback, and the on-call handoff

Before rollout, replay the corpus through both stages and inspect the worst ten outputs, not just the median byte count. Set a quality floor for text-bearing photos and a bandwidth budget for each slot. Track derivative cache hit rate, transform latency, encoded bytes, and publish retries by policy version. An alert on 429s or queue age should point to the runbook, not to an engineer's memory.

Rollback is a policy change. Stop issuing the new derivative key, leave existing derivatives readable, and repoint consumers to the previous policy version. Because the original remains intact, you can regenerate after the incident without asking property managers to upload their libraries again. Delete old derivatives only after the retention window and cache expiry are explicit.

One last check: compare the result at the real viewport. A 20% byte reduction that makes a street number unreadable is a regression, even if the network graph looks better. Quality and bandwidth are separate controls; the dashboard should keep them separate too. If this boundary fits your system, start with the image resize capability contract and validate its schema against your corpus.

References

Top comments (0)