Send smaller first batch (5 features) for faster first paint

Subsequent batches use the normal batch_size (default 50). This
reduces server-side time-to-first-property by ~10x since only 5
features need to be serialized before the first yield.
This commit is contained in:
Viktor Barzin 2026-02-22 13:24:01 +00:00
parent 9179456bf7
commit e99006e2f9
No known key found for this signature in database
GPG key ID: 0EB088298288D958
2 changed files with 40 additions and 4 deletions

View file

@ -53,6 +53,7 @@ configure_logging("api")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_BATCH_SIZE = 50 DEFAULT_BATCH_SIZE = 50
FIRST_BATCH_SIZE = 5
_rate_limit_config = RateLimitConfig.from_env() _rate_limit_config = RateLimitConfig.from_env()
@ -288,6 +289,7 @@ async def _stream_from_cache(
}) + "\n" }) + "\n"
count = 0 count = 0
is_first_batch = True
for feature_batch in get_cached_features(query_parameters, batch_size=batch_size): for feature_batch in get_cached_features(query_parameters, batch_size=batch_size):
# Apply decision filtering # Apply decision filtering
if decision_filter != "everything": if decision_filter != "everything":
@ -302,9 +304,27 @@ async def _stream_from_cache(
] ]
if limit and count + len(feature_batch) > limit: if limit and count + len(feature_batch) > limit:
feature_batch = feature_batch[:limit - count] feature_batch = feature_batch[:limit - count]
count += len(feature_batch)
if feature_batch: # Split first batch into smaller primer batch and remainder
yield json.dumps({"type": "batch", "features": feature_batch}) + "\n" if is_first_batch and len(feature_batch) > FIRST_BATCH_SIZE:
# Yield primer batch
first_features = feature_batch[:FIRST_BATCH_SIZE]
count += len(first_features)
yield json.dumps({"type": "batch", "features": first_features}) + "\n"
# Yield remainder
remaining_features = feature_batch[FIRST_BATCH_SIZE:]
count += len(remaining_features)
if remaining_features:
yield json.dumps({"type": "batch", "features": remaining_features}) + "\n"
is_first_batch = False
else:
# Normal batch yielding
count += len(feature_batch)
if feature_batch:
yield json.dumps({"type": "batch", "features": feature_batch}) + "\n"
is_first_batch = False
if limit and count >= limit: if limit and count >= limit:
break break
@ -341,6 +361,7 @@ async def _stream_from_db(
try: try:
count = 0 count = 0
batch: list[dict] = [] batch: list[dict] = []
current_batch_target = FIRST_BATCH_SIZE # Start with smaller first batch
for row in repository.stream_listings_optimized( for row in repository.stream_listings_optimized(
query_parameters, limit=limit, page_size=batch_size query_parameters, limit=limit, page_size=batch_size
): ):
@ -359,11 +380,13 @@ async def _stream_from_db(
batch.append(feature) batch.append(feature)
count += 1 count += 1
if len(batch) >= batch_size: if len(batch) >= current_batch_target:
if staging_key: if staging_key:
cache_features_batch_staged(staging_key, batch) cache_features_batch_staged(staging_key, batch)
yield json.dumps({"type": "batch", "features": batch}) + "\n" yield json.dumps({"type": "batch", "features": batch}) + "\n"
batch = [] batch = []
# After first batch, use normal batch size
current_batch_target = batch_size
if batch: if batch:
if staging_key: if staging_key:

View file

@ -302,3 +302,16 @@ class TestStreamingEndpoint:
assert feature["properties"]["qm"] is None assert feature["properties"]["qm"] is None
assert feature["properties"]["qmprice"] is None assert feature["properties"]["qmprice"] is None
def test_first_batch_is_smaller(self, client, mock_repository):
"""Test that the first batch is smaller than subsequent batches for fast first paint."""
response = client.get("/api/listing_geojson/stream?listing_type=RENT&batch_size=50&limit=10")
lines = response.text.strip().split("\n")
messages = [json.loads(line) for line in lines]
batch_messages = [m for m in messages if m["type"] == "batch"]
assert len(batch_messages) >= 1
# First batch should contain FIRST_BATCH_SIZE (5) or fewer features
from api.app import FIRST_BATCH_SIZE
assert len(batch_messages[0]["features"]) <= FIRST_BATCH_SIZE