Resource packs are the quickest way to make a server look like itself: a custom GUI, retextured blocks, a title screen with your logo. They are also the quickest way to add thirty seconds to every join and a support ticket that reads “my game freezes when I join.” This guide is for the people who maintain that pack — owners, admins, and the one staff member who knows how to open a zip file — and its goal is simple: help you ship something that still loads on a five-year-old laptop.
We rebuilt a sample pack from a mid-size survival network, timed cold loads on three client machines, and went to primary sources instead of folklore: the vanilla resource pack format documentation, PackSquash’s repository, Paper’s docs, and Blockbench’s export notes. Every number below is directional. Measure your own pack before you trust any of it.
Measure Before You Optimize
Most pack work starts with a tool and ends with a shrug, because nobody wrote down the starting point. Capture four numbers before you touch a texture, and keep them next to the release date.
- Compressed size — what a player downloads. This is your bandwidth bill and your worst-case first join.
- Resolution profile — how many files are above 16×16, and how many are 512x or larger. A handful of 2048x GUI sheets outweighs a thousand vanilla-sized blocks.
- Audio weight — total size of the sounds folders. Uncompressed WAV files are the usual culprit.
- Cold load time — clear the client’s pack cache, join, and time from “joining world” to a playable screen. Three runs, middle value.
| Metric | Sample pack before | After a full pass | What it usually means |
|---|---|---|---|
| Zip size | Roughly 300–350 MB | Roughly 60–90 MB | Bandwidth and first-join wait drop together |
| Files over 256x | Hundreds | Dozens | VRAM and atlas stitching cost |
| Audio folder | Roughly 80 MB | Under 20 MB | OGG conversion plus trimming |
| Cold load time | 30–45 s | 8–15 s | Mostly decoding and upload to GPU |
Those ranges come from one pack on three test machines. Treat the table as a map of where savings live, then find yours.
Textures: PNG Size, Atlas Layout, and Mipmaps
Compress PNGs, but know where lossy stops being free
Start lossless. Oxipng and PackSquash’s PNG recompression strip metadata and pick better filter combinations without touching a pixel. That buys a modest cut with zero risk, and it will not rescue a pack full of 2048x renders, because the pixels are the problem.
Lossy quantization reduces each image to a palette. It is acceptable on noisy surfaces players never read closely: stone variants, grass, dirt, foliage, distant terrain, particle sheets. It is a bad idea on text, GUI icons, fonts, and anything with a hard one-pixel border, where banding looks like a bug rather than a style. Quantize in a branch, then check a shop GUI, a sign, and the inventory screen at default scale before merging.
Padding, atlas layout, and mipmap bleeding
Texture bleeding — seams, flickering edges, colors leaking from a neighboring tile — is a layout problem, not a compression one. The client stitches block textures into an atlas and builds mipmaps from it, and at lower mip levels a tile samples pixels outside its own bounds. Packs that mix 16x and 128x art invite it.
- Keep tiles square and consistently sized within a block family so the atlas packs cleanly.
- Set padding in your atlas definition. Modern versions read it from
assets/minecraft/atlases/blocks.json, and the schema has changed since 1.19.3 — confirm field names against the wiki page for your target version instead of copying an old tutorial. - Match resolution where you can afford it. A 16x tile beside 128x neighbors is the classic bleeding case.
- Test at mipmap levels 0 and 4. Players control that setting and you cannot force it server-side.
Audio and Asset Cleanup
Convert audio to OGG Vorbis and trim the silence
The vanilla sound engine reads OGG Vorbis. WAV files in a pack are ignored by the client, yet authors ship them anyway — you pay for the bytes and hear nothing. Convert with ffmpeg, keep mono for positional sounds so the client can place them in 3D space, and reserve stereo for music and stingers.
ffmpeg -i raw/click.wav -ac 1 -c:a libvorbis -q:a 4 -ar 44100 assets/minecraft/sounds/custom/click.ogg
Then trim. A two-second sting with 400 ms of leading silence feels broken in game and costs bytes in every client cache. Batch-trim leading and trailing silence, and delete the source WAVs from the build folder rather than leaving them beside the OGGs.
Delete what nothing references
Every pack maintained for a year contains orphans. Walk the model JSON files, collect every textures/ and parent reference, and diff that list against the files on disk; whatever is left over may still be decoded during pack load. The same pass finds duplicate models: three copies of one custom sword under different filenames, each pointing at the same texture. Collapse them into a single model with overrides. PackSquash also prunes files that match vanilla exactly, a free win if your pack began as a full vanilla copy.
A PackSquash Pipeline You Can Run in CI
PackSquash is the practical default: it recompresses PNGs, transcodes audio to OGG, rewrites model and atlas references, prunes redundant files, and can strip data a given pack_format never reads. It is fast enough to run on every commit, which is the real value — optimization becomes a property of the pipeline instead of a chore someone remembers twice a year.
# pack.toml (trimmed; see the repository README for the full schema)
[output_file]
path = "build/server-pack.zip"
[compression]
zip_spec = "deflate"
compression_level = 9
[optimize]
lossless_png = true
transcode_ogg = true
color_quantization_target = "auto"
A GitHub Actions job that gates a pull request on pack size and publishes the hash your deploy step needs:
name: build-pack
on: [push, pull_request]
jobs:
pack:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install PackSquash
run: |
curl -L -o ps.zip https://github.com/ComunidadAylas/PackSquash/releases/latest/download/packsquash-x86_64-unknown-linux-musl.zip
unzip -o ps.zip -d /usr/local/bin && chmod +x /usr/local/bin/packsquash
- name: Build optimized pack
run: packsquash --config pack.toml pack/ build/server-pack.zip
- name: Record size and hash
run: |
ls -lh build/server-pack.zip
sha1sum build/server-pack.zip | tee build/sha1.txt
Check the README for the version you pin; flag names move between releases.
Delivery: CDN, Caching, MIME Types, and SHA-1
A small pack served badly still feels slow. Put the artifact behind a CDN or object storage, not on the box running your world, and set Content-Type: application/zip explicitly — some stacks guess application/octet-stream, and players behind strict proxies get a download prompt on the join screen. Leave range requests on so a failed download can resume.
- Hash the filename on deploy.
server-pack-3f9a1c.zipcan be cached forever at the edge;pack.zipcannot, so every deploy is a cold cache for every player. - Update the SHA-1 every time. A hash that does not match the served bytes makes clients re-download on every join — the usual cause of “the pack updates constantly.”
- Keep the previous artifact up for a few days so joins in flight during a deploy do not fail mid-download.
- Check proxy behavior. BungeeCord, Velocity, and pack-forwarding plugins each handle the resource pack fields differently.
None of this touches tick time — for that, see [Understanding MSPT and Tick Rate: A Server Owner’s Performance Guide]. Pack work is a client-side and network-side optimization, and its natural partner is [Resource Pack Basics: Hosting, Distribution, and Forcing Packs].
Client memory, VRAM, and Bedrock via Geyser
Every high-resolution texture is uploaded to GPU memory and held for the session. A pack of 512x sheets can push a 2 GB integrated GPU into swapping, and the symptom is not a crash — it is stutter while flying, late chunk rendering, and a client that dies on alt-tab.
Bedrock changes the math. Geyser can forward packs, but Bedrock’s format is different: no pack.mcmeta, a different manifest, no Java model JSON. Anything you force through Geyser needs a separate Bedrock build, and mobile size ceilings are lower than people expect. Client pressure from mob counts is covered in [Taming Entity Counts: Reducing Mob Lag on Busy SMPs].
Pros and Cons of an Aggressive Optimization Pass
- Pro: Faster joins and fewer “my game froze” reports, especially from laptop players.
- Pro: Lower egress cost and a CDN bill that stops scaling with player count.
- Pro: A repeatable build means pack changes are reviewed, hashed, and deployable by anyone on staff.
- Con: Lossy quantization is irreversible if you delete the source textures. Keep the source project in version control.
- Con: Pruning breaks things quietly — a sound referenced only from a plugin path can vanish with no build error.
Common Mistakes and How to Avoid Them
- Optimizing before measuring. Keep a baseline row per release. It settles every later argument.
- Forgetting the SHA-1. Wire the hash into deploy automation; a stale hash is invisible in testing and obvious to players.
- Quantizing GUI and fonts. Text is where banding shows instantly. Keep those files lossless.
- Testing only on your own machine. Your desktop is the best-case client. Ask a low-end laptop user to join after each major change.
- Bundling the source folder. The zip should contain only what the client reads. Editors, PSDs, and
.bbmodelfiles belong in git.
One tip from the rebuild: test on a client with no cache and a server that forces the pack. The bleeding atlas and the stale hash both appeared only on a cold join.
Frequently Asked Questions
Aim for under 50 MB for a forced pack on a public server, and under 100 MB only when the pack is optional. Above that, first joins get slow and mobile Bedrock clients struggle.
Yes. It recompresses PNGs, transcodes audio to OGG, prunes files that match vanilla, and rewrites references so the pack still works. Run it in CI so every build is optimized the same way.
Texture bleeding comes from atlas packing and mipmaps, not compression. Keep tile sizes consistent, add padding in your atlas definition, and test with the client mipmap setting at 4.
Rarely. The pack is processed on the client and delivered over HTTP, so server tick time is mostly unaffected. The cost shows up as join-time delay, client RAM and VRAM use, and CDN bandwidth.
Start With One Measurement
You do not need a full rewrite to fix a slow pack. Record the four baseline numbers, convert the audio folder, delete orphans, drop PackSquash into your build, then serve the artifact from a CDN with a hashed filename and a matching SHA-1.
If you are still deciding how the pack reaches players, read [Resource Pack Basics: Hosting, Distribution, and Forcing Packs] first. For model authoring, Blockbench exports are the cleanest starting point — check its texture and UV options before you wonder why a model looks stretched in game.

Leave a Reply