Administrator reviewing image thumbnails on a monitor beside a phone showing a compact photo in a thread

A forum’s first-screen weight is usually a picture: the logo, a hero, the first attachment in a popular thread, a gallery strip. XenForo 2.3 will convert JPEG / PNG / BMP uploads to WebP, resize in the browser before the file hits PHP, and refuse (or skip resize on) sources above a pixel-count cap. Discourse will let a brand-new TL0 user embed one image and will serve uploads from local disk, from S3, or from a CDN — and those three URLs are not the same setting. This page is that machinery.

This is not the Core Web Vitals / speed article. That one is guest page cache, Symfony Cache, icon sprites, Gzip, Cloudflare, and the first 60 minutes when the live board is slow. Read it for LCP / INP / CLS as a system. Stay here for bytes that are photographs. It is also not the responsive pass (layout) or the typography file-hosting walkthrough (letters). Images without reserved size still cause CLS; we will say that once and then point back.

Official XenForo sources: 2.3 WebP conversion, client-side HTML5 resize, $config['maxImageResizePixelCount'] default 20000000 (raise to 48000000 for larger sources), Setup → Options → Image and link proxy, $config['proxyUrlFormat'], Media Gallery EXIF behaviour. Discourse: TL0 newuser max embedded media = 1, S3 uploads versus DISCOURSE_S3_CDN_URL versus DISCOURSE_CDN_URL. Notebook figure: WebP 52.5% storage cut — treat as a vendor / notebook number, not a measurement we ran on your attachments.

What “optimized” means on a thread

Members do not say LCP. They say the thread took three seconds to show the screenshot, the phone used a day’s data, or the page jumped when the image arrived.

Symptom Usual cause First control
Hero or first image late Unconverted JPEG, remote hotlink, no guest cache WebP + local host + CWV cache
Upload fails / never resizes Pixel count above the cap; PHP memory maxImageResizePixelCount; client-side resize
Mixed-content warnings http:// embed on https:// board Image proxy; fix Board URL
New user cannot post four screenshots Discourse TL0 cap Welcome text; maybe raise newuser max embedded media one notch
Images 200 ms from Tokyo, 4 s from Sydney No CDN, or the wrong CDN URL App CDN ≠ S3 CDN

Do not chase a PageSpeed 100 on a logged-in thread. Aim for: new uploads are WebP (or already small), old whales are scheduled, the proxy is not an open cache of the internet, and a TL0 user knows the one-image rule before they write the intro.

XenForo 2.3: WebP, with an honest percentage

When you enable it, new JPEG, PNG, and BMP uploads become WebP. Official 2.3 materials and the notebook claim about 52.5% less attachment storage once you also rebuild existing files. That is a vendor / notebook figure. It is not a Styles Factory benchmark. Your screenshot dump, your camera originals, and your already-compressed memes will not hit the same number. Treat 52.5% as a direction: WebP is worth turning on. Do not put “we halved disk” on the marketing page until you measured data/ before and after.

Enable the conversion in the 2.3 attachment / image options (the ACP label in your build is the one under Setup → Options for attachments / images — believe the screen). Then:

  • New uploads convert going forward. That is the cheap win. Do it after the board is stable, not in the same hour as a schema upgrade. The 2.3 upgrade article already sequenced that.
  • Historical rebuild is a job: Tools cache / rebuild jobs, or the CLI. Official warning: old PHP + GD has known image-processing bugs. One job. Off-peak. Do not start “rebuild all attachments as WebP” and “rebuild search index” together at 20:00.
  • Animated GIFs. Confirm a sample still opens. Do not assume a GIF you wanted as animation became a single WebP frame and call that a win without looking.
  • Already-WebP / already-AVIF sources. Conversion is for JPEG / PNG / BMP in the official description. Do not invent a second converter.

WebP is not a CDN. It is not guest page cache. It is smaller files on disk and on the wire. The speed article is what stops PHP from rebuilding the page around those files.

Client-side resize (do this)

The attachment manager can shrink the image in the browser with HTML5 before upload. Official reason: less server CPU, less visitor bandwidth. Turn it on.

Members with 12 MP phone photos will not notice. Your data/attachments directory will. The server-side resizer still exists for clients that cannot do the HTML5 path and for anything that sneaks past. Client-side is how you stop paying PHP to decode a 4000×3000 vacation photo on every upload during a launch week.

Test as a throwaway on a phone: pick a camera original, watch the composer, confirm the board accepted it. If it fails, you are in the next section, not in “disable client-side resize.”

$config['maxImageResizePixelCount']

In src/config.php:

$config['maxImageResizePixelCount'] = 20000000;

20,000,000 is the official default. Images above that are not resized and may be rejected. Official note: raising it to 48,000,000 lets the memory engine handle larger sources.

Width × height is the count. A 6000×4000 photo is 24,000,000 pixels — already over the default. A photographer community will hit this in week one. A text community will not.

Raising the cap also lets one upload spike PHP memory. Raise it because you measured a failure, not because a blog said a bigger number is faster. 48,000,000 is the documented example, not a challenge to pick 200,000,000.

Worked policy:

Community Cap Notes
Support / game / general Leave 20000000 Client-side resize on; tell people to downscale
Photo / art, staff uploads of camera originals 48000000 if you saw rejects Watch PHP memory_limit; rebuild jobs off-peak
“Just make it infinite” Do not One 100 MP file is a support ticket and a memory conversation

Pair the cap with a human sentence in the posting guidelines: “If the board rejects the photo, it is too many pixels; shrink it and try again.” Do not make them guess.

Setup → Options → Image and link proxy. XenForo’s HTTP client fetches remote images so your pages do not hotlink (and so mixed-content https pages do not break). Config hook:

$config['proxyUrlFormat'] = 'proxy.php?{type}={url}&hash={hash}';

That is the official format string. Leave it unless you know why you are changing it.

The proxy is a privacy and reliability feature. It is also a cache of other people’s bytes on your disk. If you allow untrusted BB-code images, watch the proxy store. A board that lets guests embed arbitrary URLs plus a proxy that never expires is how internal_data grows a copy of the internet.

Practical rules:

  • HTTPS Board URL first. The proxy is not a substitute for a certificate.
  • Hotlinked images from a flaky host are why the proxy exists. Keep it on for user-posted remote images.
  • Staff-curated portal heroes should be local attachments or style assets, not proxied remote files. You want those bytes on an origin you control (and on the CWV cache path).
  • If the proxy store is the disk problem, that is an ops pass (expiry, disk), not a reason to turn HTTPS off.

Thumbnail sizes and native lazy-loading are not in the official notebook we used for this series. Do not invent ACP labels for them. Give images width and height in templates you control so CLS stays down. If you add a lazy-load add-on, community sources warn it fights third-party portals and sliders (XenPorta 2 is the named example): the slider initializes before the image exists, or never initializes. Test the first screen after any lazy-load change.

XFMG: EXIF, then the WebP path

XenForo Media Gallery is an official add-on (purchased separately on self-host; included in Cloud plans). Notebook / official behaviour worth writing down:

  • EXIF is preserved before resize. Camera metadata (including GPS if the phone left it on) is not something you should assume is stripped just because the file got smaller.
  • The gallery uses the WebP conversion path once that is on.

Implications:

  • Publish a gallery policy: whether location data is okay in public albums. “We convert to WebP” is not that policy.
  • Rebuilds that touch gallery files are still jobs. Same off-peak rule.
  • A photography board that needs full-resolution originals may also be the board that raises maxImageResizePixelCount. Those two decisions belong in the same staff thread.

This page will not invent XFMG option names that were not in the notebook. Open Setup / the gallery options in your ACP and believe those labels.

Logos, heroes, and the images you ship

Member uploads are half the story. The other half is the style.

  • Logo / mark. Serve a sized asset, not a 4000 px PNG in the header. SVG or a compact WebP/AVIF at the display size. The header is on every page; it is LCP more often than people think.
  • PWA icons. Official: 192 and 512, square PNG, ideally maskable. PWA / push. Those files are not the header logo.
  • Hero / slider. One image, already the width you display, or no slider. A news homepage that loads four 2000 px JPEGs before the thread list is a self-own. The landing page article is the IA; this page is the bytes.
  • Custom fonts are not images; they still compete for the same first-screen budget. Host them yourself. Do not add a remote webfont CDN. See typography.
  • extra.less background images. A 1.8 MB JPG as a full-page background will not flip with style variations unless you write that flip, and it will wreck mobile. Prefer a palette chip.

Icons are SVG sprites in 2.3. Run the Icon Usage Analyzer. A raw content: "\f019" in extra.less misses the subsetter. That is the speed article’s icon section; do not reinstall Font Awesome Manager “because 2021.”

Discourse: one image for New users

Default Discourse sandboxes TL0 (New). Documented default: 1 embedded image per post. Setting name: newuser max embedded media.

The composer does not warn a new user, mid-draft, that the fourth screenshot will block publish. They write a long introduction, attach four photos, hit reply, and get a limit. They leave. Staff never see this because staff are not sandboxed.

This is the most common “Discourse is hostile” ticket in month one. It is a documentation failure on your welcome topic, not a bug. The first-month catalogue already printed the fix. Repeat because this is the image article:

  1. Leave the sandbox on.
  2. Welcome topic: “Until the site promotes you, you get one image. Read a few topics; that is how you become TL1.”
  3. Private window, fresh user, every week of month one. Post the intro you expect.
  4. If the community must allow more on day one (photography, support screenshots), raise newuser max embedded media a notch. Do not set it to 20. Do not promote everyone to TL1 on register.

Related default-shaped limits (so you do not “fix images” and miss the next wall): 2 links (newuser max links and friends), 2 mentions, tighter chat rate (notebook: TL0 20 / 30s, TL1+ 40 / 30s, mods unlimited). Invited users default to TL1 in the documented model — invite-only rooms skip most of this pain.

Discourse: local disk, S3, and two different CDNs

Three layers people mash together.

1. Local uploads

Default Docker: uploads live on the VPS volume. Fine for a small community. Disk is the number people under-buy (official install: 10 GB minimum, 20 GB+ recommended, more if you keep local backups). The hosting cookbook owns sizing.

2. S3 (or S3-compatible) for uploads

A separate project from S3 backups. Meta: s3_backup_bucket must not be the same folder as uploads. If you only wanted offsite backups, you can omit DISCOURSE_USE_S3, DISCOURSE_S3_CDN_URL, and DISCOURSE_S3_BUCKET. Those belong to the uploads story.

Uploads-on-S3 is when local disk or guest image weight is the problem. It is not a day-two décor project.

3. Two CDN URL env vars

Variable What it fronts Rule
DISCOURSE_CDN_URL The application CDN (assets the app ships) Must include the protocol (https://…)
DISCOURSE_S3_CDN_URL The S3 uploads CDN Not the app CDN. Different host, different job

Set the app CDN when guests are fetching assets from far away, or when official hosted already includes one. Set the S3 CDN when upload bytes should come from a public front in front of the bucket. Pointing both at the same hostname “to keep it simple” is how you debug cache headers for a month.

Cloudflare in front of Discourse: disable JS minify / JS modifications or Discourse breaks. That is a security / ops fact; it is also how a “make images faster” dashboard toggle takes down the composer. Cache images at the edge. Do not let the same dashboard rewrite JavaScript.

DISCOURSE_CDN_URL without a protocol is a common mis-paste. Include https://.

CLS: reserve the hole

Core Web Vitals recap in one paragraph, then go back to the speed piece:

  • If the image has no width/height, the thread jumps when it arrives. That is CLS.
  • If the largest thing on the first screen is a 2 MB PNG logo, that is LCP.
  • If a lazy-load add-on fights a slider, the tap does nothing useful. That is INP adjacent.

In templates you control (page nodes, custom widgets), put explicit dimensions on <img>. In XenForo post bodies, the attachment system usually emits sizes; do not strip them in extra.less. In Discourse, do not CSS-override images to height: auto without a width that preserves aspect if you also set a fixed height somewhere else.

max-width: 100% on images is the responsive rule that stops horizontal scroll. Pair it with a height behaviour that does not collapse the box to zero before load.

A 60-minute pass (XenForo)

  1. Confirm WebP conversion is on for new JPEG/PNG/BMP.
  2. Client-side resize on.
  3. Read maxImageResizePixelCount. Leave 20M unless you have a reject in the log.
  4. Image and link proxy on if members may embed remote URLs. proxyUrlFormat default unless you have a reason.
  5. Guest: homepage + one image-heavy thread. Note the first image’s type (WebP or not) in DevTools.
  6. Logo file size. If it is a PNG whale, replace it this hour. Do not rebuild all historical attachments in this hour.
  7. If you run XFMG: write the EXIF / GPS sentence in the gallery rules. One sentence is enough.
  8. Schedule the historical WebP rebuild for off-peak, alone.
  9. Purge CDN if you cache attachments or data/.
  10. Optional: PageSpeed on a guest phone profile. If LCP is still a photo, you now know which URL.

A 60-minute pass (Discourse)

  1. Welcome topic states the one-image TL0 rule. Fresh-user test.
  2. newuser max embedded media only raised if the community type demands it.
  3. Uploads: still local? Fine. Planning S3? Different bucket from backups. Read Meta’s uploads topic before app.yml.
  4. If a CDN exists: is it DISCOURSE_CDN_URL (app) or DISCOURSE_S3_CDN_URL (uploads)? Protocol included?
  5. Cloudflare: JS minify / JS modifications off.
  6. One long topic with uploads, on a phone, as TL0 and as TL1. Both must be explainable.
  7. Disk: is /var/discourse the reason you thought you needed S3, or is it five local backups? Fix backups first (Meta 14855).

What usually looks like a media bug

Symptom Likely cause First move
Upload rejected, huge camera file Pixel cap Client-side resize; then 48M if you must
New files WebP, old files huge You never rebuilt Off-peak job, one at a time
Broken lock icon / mixed content Remote http:// image Proxy + HTTPS Board URL
Disk exploding Proxy cache or local Discourse backups Expiry; off-box backups
TL0 cannot post the intro One-image cap Welcome text; maybe +1, not +20
Images fast in the US, dead in AU No CDN, or S3 URL used as app CDN Split the two env vars
Composer died after “optimize JS” Cloudflare JS minify Turn it off; see security checklist
Slider empty Lazy-load vs XenPorta-class portal Test first screen; disable one

What this is not

It is not the full speed / CWV guide. Guest cache, Redis, icon sprites, and the 60-minute triage live there. This page is photographs and the switches that shrink them.

It is not a CDN vendor ranking and not an S3 price list.

It is not permission to strip EXIF as a claim we cannot document. XFMG preserves EXIF before resize. Say that. Then write a policy.

It is not a “best gallery add-on” roundup. Official gallery is XFMG. Discourse has core uploads plus optional plugins this article will not invent.

Checklist

  • XF 2.3 WebP on for new JPEG/PNG/BMP; 52.5% treated as vendor/notebook, not your number
  • Client-side resize on
  • maxImageResizePixelCount 20000000, or 48000000 after a measured reject
  • Image and link proxy understood; proxyUrlFormat only changed on purpose
  • XFMG: EXIF preserved; GPS policy written
  • Historical rebuild scheduled, not in the same hour as another heavy job
  • Logos / heroes sized; no 2 MB header PNG
  • Discourse TL0: welcome text for 1 image; newuser max embedded media raised only with a reason
  • S3 backups ≠ S3 uploads
  • DISCOURSE_CDN_URL (protocol included) ≠ DISCOURSE_S3_CDN_URL
  • Cloudflare JS minify off in front of Discourse
  • Width/height reserved where you control markup; CLS checked on a guest thread

Takeaways

  • XenForo 2.3’s image win is boring and good: WebP for new uploads, HTML5 resize before PHP, a 20M pixel cap you only raise to 48M when camera originals require it, a proxy for remote embeds, XFMG that keeps EXIF then follows the WebP path. The 52.5% storage cut is a notebook / vendor figure.
  • Discourse’s image win is mostly policy: TL0 gets one image until they graduate. The ops win is not confusing local disk, S3 uploads, DISCOURSE_S3_CDN_URL, and DISCOURSE_CDN_URL.
  • Overall LCP / INP / CLS, guest cache, and icon sprites remain the speed article. Do not rebuild every historical attachment to avoid turning on page cache.
  • Reserve space, size the files you ship yourself, and never let a CDN “optimize JavaScript” as a side effect of caring about pictures.

Turn WebP on for new files this afternoon. Write the one-image sentence tonight if you run Discourse. Measure disk next month, not in the blog post.