Skip to content

refactor: load firebase_admin lazily#279

Open
jonesnc wants to merge 10 commits intoxtrinch:masterfrom
jonesnc:lazy-firebase-admin-import
Open

refactor: load firebase_admin lazily#279
jonesnc wants to merge 10 commits intoxtrinch:masterfrom
jonesnc:lazy-firebase-admin-import

Conversation

@jonesnc
Copy link
Copy Markdown

@jonesnc jonesnc commented Jul 11, 2025

Changes

Lazily import firebase_admin and its modules because we've found it's one of the slower modules to import in our Django projects. For type annotations, the firebase_admin is only imported after checking typing.TYPE_CHECKING.

Please let me know if you have any suggestions or feedback on this, I'd be happy to implement them.

Thanks for this package, I think it's great!

Tests

Test session starts (platform: linux, Python 3.10.12, pytest 8.2.2, pytest-sugar 1.0.0)
django: version: 4.2.19, settings: tests.settings.default (from ini)
rootdir: /home/nathanjones/Projects/fcm-django
configfile: pyproject.toml
plugins: sugar-1.0.0, anyio-3.6.2, mock-3.14.1, django-4.11.1, asyncio-0.14.0, Faker-15.3.4, typeguard-2.13.3
collected 16 items

 tests/test_admin.py ✓✓                                                                                                                                                                                                                      12% █▍
 tests/test_api_rest_framework.py ✓✓✓                                                                                                                                                                                                        31% ███▎
 tests/test_api_tastypie.py ✓                                                                                                                                                                                                                38% ███▊
 tests/test_models.py ✓✓✓✓✓✓✓✓✓✓                                                                                                                                                                                                            100% ██████████

Results (3.49s):
      16 passed

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Jul 14, 2025

@xtrinch I just pushed edfc18e (#279), hopefully that fixes the formatting errors.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Jul 14, 2025

@xtrinch Sorry, I missed one error. I just fixed that one as well.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Jul 14, 2025

@xtrinch I'm not sure how to fix the 3.7 action. onnx/tensorflow-onnx#2376 suggests limits 3.7 to ubuntu 22.04, maybe that's the solution?

Let me know if there's anything I can do to help.

@xtrinch
Copy link
Copy Markdown
Owner

xtrinch commented Jul 15, 2025

Python 3.7 has reached its end of life, I believe we can safely remove it.
Other than that, I'd like to hear some more opinions from people on this PR, whether this is something that more people would like to see happen.
I'd also like to hear what magnitude of a slowdown you are experiencing with eagerly importing firebase_admin.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Jul 15, 2025

I'd also like to hear what magnitude of a slowdown you are experiencing with eagerly importing firebase_admin.

According to python -X importtime manage.py runserver --noreload, it's adding about 120ms to our total startup time. In my Django project, that's ~6% of the current total Django start/restart time when running makemigrations or runserver, for example.

In isolation, that probably doesn't sound like a lot, but I work on a project with a lot of different dependencies, so I'm always looking for ways to improve the import/setup speed of those dependencies. We tend to accumulate a lot of performance benefit by optimizing many of our different imports, so every little bit counts to the overall goal of improving django start time.

For example, if we can improve the import time of 5 different dependencies that each take about 100ms to import and firebase_admin is one of those in this example, that's a total of 500ms startup improvement, which means our dev experience is 500ms faster. That's my intent with this change.

Thanks for your time and consideration.

@ahmedbilal
Copy link
Copy Markdown

@jonesnc This latest release 2.3.1 bump up the ceiling for firebase_admin constraint allowing firebase_admin 7 as dependency which removed the dependency on google-api-python-client thus significantly reducing the overall footprint (memory + size) of this package. Can you run your profiling again and see whether you need these changes? Thanks.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Jul 29, 2025

It looks like my project will take some work to update to firebase_admin version 7, so I can't provide any importtime benchmark results at this time. I'll work on this and get back with you.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Oct 6, 2025

@ahmedbilal I finally did some testing by upgrading to version 2.3.1, and found that while removing the googleapiclient import saved ~35ms import time (which is appreciated), the firebase_admin package import is still taking up ~134ms import time. I'd still consider that a win in my book.

I'll go ahead and fix the branch conflicts.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Jan 23, 2026

Hi @ahmedbilal @xtrinch, I pushed some updates here.

I added a new INITIALIZE_APP_CALLABLE setting that defers Firebase initialization until the first FCM operation. This is useful when credentials aren't available at startup—for example, when fetching them from a secrets manager at runtime, or when lazy loading of the firebase_admin module is desired.

The INITIALIZE_APP_CALLABLE Callable is called on first use and the resulting firebase_admin.App object is cached. Also added documentation to the README.

One thing to note: the cached app uses a module-level global without locking, so there's a potential race condition if multiple threads hit the first FCM call simultaneously. In practice this probably just means the callable gets invoked more than once before the cache is set. Let me know if you'd like me to add thread-safe locking if this is not acceptable.

Please let me know if you have any suggestions or concerns about this approach. Thanks for your time.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Feb 9, 2026

@ahmedbilal @xtrinch Any updates on this?

@xtrinch
Copy link
Copy Markdown
Owner

xtrinch commented Apr 2, 2026

@jonesnc Alright I have cleaned this up so we dont do lazy imports all across the board, wrapped them in some wrapper functions, renamed the setting parameter, and I think it's good to go. I think the use cases justify this even though it does kind of increase complexity of the codebase. Do a quick check please

@xtrinch xtrinch force-pushed the lazy-firebase-admin-import branch from a9fc755 to 4375c26 Compare April 3, 2026 10:58
@ahmedbilal
Copy link
Copy Markdown

@jonesnc @xtrinch Not sure whether this would help this cause or not, but see https://docs.python.org/3.15/whatsnew/3.15.html#pep-810-explicit-lazy-imports

@xtrinch
Copy link
Copy Markdown
Owner

xtrinch commented Apr 9, 2026

@ahmedbilal This will only be useful once Python ≥3.15 becomes the baseline (e.g. in LTS environments). Right now, it doesn’t help us.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Apr 28, 2026

I'm happy to just wait until Oct 1, 2026, when Python 3.15 is released. Would that be a good time to revisit this, or do you want to wait for a later date?

@xtrinch
Copy link
Copy Markdown
Owner

xtrinch commented Apr 29, 2026

@jonesnc Yes, that works, but we'd need to make sure it still works for earlier python versions, even if make lazy imports work for python 3.15.

@jonesnc
Copy link
Copy Markdown
Author

jonesnc commented Apr 29, 2026

@xtrinch From PEP 810:

For code that cannot use the lazy keyword directly (for example, when supporting Python versions older than 3.15 while still using lazy imports on 3.15+), a module can define lazy_modules as a container of fully qualified module name strings. Regular import statements for those modules are then treated as lazy, with the same semantics as the lazy keyword:

__lazy_modules__ = ["json", "pathlib"]

import json     # lazy
import os       # still eager

So we could use __lazy_modules__ instead of the lazy import syntax, which would be compatible with previous python versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants